# Introduction

**What is Mystic**

Mystic makes it easy for anyone to build their own experiences on top of Morpho, both by leveraging the Mystic UI and by using the Mystic API. We expand the protocol to new ecosystems, help Foundations, curators and funds build their own experiences and work as an infra provider for those building on the Morpho stack. For more information, see our [API](/api/mystic-morpho-rest-api).

Are you building something on Morpho? Build it 10x faster by using the Mystic API -> [contact us here.](mailto:joao.moreira@mysticfinance.xyz)

**What is Morpho**

Mystic is a lending protocol built on the [Morpho](https://morpho.org/) stack. The core of Morpho is comprised of lending markets (Morpho markets), each a pool with solely a collateral asset and a borrow asset. This enables very specific risk management parameters to be set for each pair. Third-party risk curators can then create vaults which accept deposits of a single asset and dynamically allocate them to several markets based on predefined risk parameters and optimization strategies. To know more, see [Morpho Vaults](https://docs.morpho.org/overview/concepts/vault/).


# Core Pools

Core pools are Mystic's main markets. They are made up of blue-chip assets exclusively and share risk and liquidity with each other. Do mind, this means that supplying to a core pool means gaining exposure to the core pools market at large. These core pools are the base layer of Mystic and work similarly to other modern lending markets. Some key things to consider in core pools include:

* **Isolation Mode**: Allows assets with higher risk profiles to be safely used as collateral in isolated lending environments.
* **eMode (Efficient Mode)**: Enhances borrowing power for assets with correlated price movements.
* **Supply and Borrow Caps**: Limits supply/borrow for better risk management in a specific asset.

This core layer of Mystic enables boosting efficiency and liquidity for core assets on Plume while maintaining a sturdy risk management profile.

Want to see how you can get started with core pools? See our [User Guide](/core-concepts/lending).


# Curated Vaults

### Curated Vaults

RWAs are a long-tail game, where assets are significantly less liquid than their digital assets counterpart. As such, isolating risk and adjusting to each asset's characteristics is paramount in RWA lending.

That is why Mystic has taken the core infrastructure decision to build on top of [Morpho](https://morpho.org/). The core of Mystic is comprised of lending markets (Morpho markets), each a pool with solely a collateral asset and a borrow asset. This enables very specific risk management parameters to be set for each pair. Third-party risk curators can then create vaults which accept deposits of a single asset and dynamically allocate them to several markets based on predefined risk parameters and optimization strategies.

This not only makes for a simplified UX, it gives users institutional-grade risk management and transparency on the markets. Curated vaults have been built on top of [Morpho](https://morpho.org/). To know more, see [Morpho Vaults](https://docs.morpho.org/overview/concepts/vault/).

### Mystic Morpho Curated Vaults

Mystic leverages Morpho's infrastructure and takes it to the next level to best fit RWAs and their characteristics. All of Mystic's other features integrate directly with Morpho, meaning:

* Each pair tied to isolated pools on Mystic can be leveraged via [leverage strategies](/overview/leverage-strategies), enabling users to seamlessly leverage on these markets.


# Custom Permissioning

As we are dealing with securities when doing RWA pools, we must ensure to comply with all relevant Anti-Money Laundering (AML) regulations, which dictate only KYC'd entities may hold or trade securities. To accommodate for that, we must ensure only eligible parties ever hold RWAs on Mystic.

Our solution to this is to set access rules individually per pool, depending on its assets' requirements (e.g. only non-US investors can supply or borrow from a specific pool). To support all asset classes whilst still making markets as permissionless as possible, three levels of access are possible:

* Permissionless - no one needs to KYC/KYB, as the asset is freely transferable.&#x20;
* Semi-permissioned - only borrowers and liquidators must KYC/KYB - if there are liquidators in the pool and we know lenders will never touch the asset, then supplying may be permissionless. If borrowers have KYC'd with issuer already, Mystic will read from their whitelist to prevent them having to KYC again.
* Permissioned - all participants must KYC/KYB, due to asset, issuer or market requirements.

Mystic ties access to each market to roles, which are then attributed based on KYC/KYB information upon user registration or partner KYC whitelist integration.

### Custody

Since Mystic is not a registered broker, we must keep all securities with a custodian. This is also better for borrowers from a risk perspective, as they know their assets are being safekept with a custodian during their interaction with the protocol. Our approach is quite simple and elegant - we have integrated our protocol with a custodian, such that whenever an RWA is locked/deposited on our pools, it is sent to a custodian for safekeeping. We do, however, also enable assets to be remain on our smart contract. This combination of centralisation and decentralisation is one of the things that makes Mystic so appealing and which makes us so uniquely positioned to bridge TradFi and DeFi.

By default, the protocol is fully non-custodial. This feature exists as a backstop for more illiquid or sensible collateral.


# Instant Liquidity API

## What Is It

The Mystic Instant Liquidity API gathers DEX liquidity and trading firm bids to find the best possible path to give instant liquidity to any supported asset, via a transparent and competitive auction mechanism. This can be used for a) instant redemptions or b) liquidating money market defaults that fallback to non-instant redemption mechanisms.

The API is custom-built for RWAs, and assumes trading firms advance the liquidity to fill the order and receive the corresponding collateral plus a premium for doing so. They can then redeem that asset for stablecoins after waiting for the asset's corresponding redemption time. The API is modeled after the[ 0x Protocol Orderbook](https://docs.0xprotocol.org/en/latest/basics/orders.html#the-orderbook) and[ RFQ Orders](https://docs.0xprotocol.org/en/latest/basics/orders.html#rfq-orders), and adapted for RWAs. Settlement is atomic, making sure no capital is at risk and only moves when a swap is confirmed.

A couple of things to note:

* The API has a **smart routing** feature, which enables it to settle a position across multiple bids (e.g. 20% on an AMM and 80% with the best trading firm quote).
* The API also has a **fallback feature** - if the winning bid backs out, the trade will go to the second-best bid and so on.


# How It Works

## Roles

There are two roles in the Mystic RFQ process:

* **The Solver** - the RFQ participant that is providing short-term liquidity for the issuer;
* **The Manager** - the issuer or platform that has created the auction and can set its parameters: the premiums the Solvers receive, what entities can participate in each auction (e.g. US entities only), if the auction is public or private.

To note that Solvers only engage with Managers and vice-versa. Mystic never touches any asset.

## Process Overview

There are 4 stages to the lifecycle of an auction on Mystic via the RFQ API, namely:

1. **Detection** - Mystic adds an instant redemption request or money market liquidation to its feed, for Solvers to see;
2. **Order Submission** - Solvers send Mystic their bids;
3. **Solver Selection** - Mystic assesses all bids and chooses the best one;
4. **Settlement** - Winning bid settles the outstanding opportunity.

All of this happens in milliseconds, giving the users an instant UX. Let us now look at each step in more detail:

### 1. Detection

The protocol continuously monitors requests by instant redemption partners and the relevant money market accounts to be liquidated. In the case of liquidations, when an account’s Health Factor reaches < 1, it is flagged as liquidatable and added to our API feed. As for instant redemption requests, when an integrated Manager receives a request, that request is then echoed to Solvers via the Mystic API.

### 2. Order Submission

Solvers query the feed, evaluate available RFQ opportunities and respond with signed RFQ orders (via POST /rfq-order). Orders use token permits so Solvers don’t need to transfer tokens upfront.

### 3. Solver Selection

Mystic evaluates all quotes and selects the best order for each opportunity (read, best price). To note, multiple RFQ fills may occur per opportunity.

### 4. Settlement Contract Flow

Settlement can occur for either liquidations or for instant redemptions. Let's first look at the flow for liquidations for a Morpho market, which will happen atomically through a flashloan:

1. Mystic selects the best RFQ order(s) for the underwater account;
2. Via [Bundler](https://docs.morpho.org/learn/concepts/bundlers/), Mystic calls GeneralAdapter.morphoFlashloan() to borrow the amount needed to repay the outstanding debt;
3. Inside the flashloan callback, Morpho.liquidate(account, borrowedAsset, collateralAsset, amount) is called using the flash-borrowed funds, which releases the position's collateral;
4. Still inside the callback, Mystic calls the RFQ contract's 0xSettlement.fillRFQOrder(order, sig) with the winning Solver's bid and token permit, effectively giving them the trade and sending them the position's collateral in exchange for the position's debt asset (e.g. USDC);
5. Mystic repays flash loan with the assets received from Solver;
6. Fees are distributed to the Solver minus a protocol fee;
7. Solver redeems the collateral for their original asset (optional).

As for the case of an instant redemption, the flow is much simpler:

1. Mystic selects the best RFQ order(s) for instant redemption request;
2. Mystic calls the RFQ contract's 0xSettlement.fillRFQOrder(order, sig) with the winning Solver's bid and token permit, effectively giving them the trade and sending them the asset to be redeemed in exchange for their assets (e.g. USDC);
3. Fees are distributed to the Solver minus a protocol fee;
4. Solver redeems the redeemed asset for their original asset (optional).


# How To Integrate

### Overview

Solvers on the network receive information via continuous GET requests on Mystic's API to know when an instant redemption request or liquidation happens. They then sign and send a quote with the price they offer to settle the trade at. Mystic will then choose the one with the best price, give it the trade and settle the transaction. Here we go over how a solver can integrate with Mystic to receive our trade requests and send us their bids. There are 4 steps to follow:

* Register as a Solver;
* Query Requests;
* (Optional) Approve the Mystic contract;
* Sign and send quote.

Let us now look at how to do each one.

### Register as a Solver

Anyone can see open RFQs, but only registered Solvers can bid on them. To participate in the Mystic RFQ process, you first need to register as a Solver with our team. It's a straightforward process, all we need is your firm's name, the address of the wallet you'll be bidding with (and on what chain) and a general awareness of what RFQ's interest you. To register as a Solver, [contact us via email](mailto:joao.moreira@mysticfinance.xyz).

### Query Redemption Requests

To see all requests you can bid on at any moment, all you have to do is query this API endpoint according to our [API Reference](https://curator-api.mysticfinance.xyz/docs/#/rfq):

```
https://api.mysticfinance.xyz/redemption/requests
```

This is the response you would receive, assuming you see only one redemption request for WPLUME:

```
{
      "account": "0x8480b3aa300dd7fad979d31270732040011a6ca7",
      "apy": "0",
      "riskRating": "BBB",
      "chainId": 98867,
      "requestId": "6caa1f2f-0743-4470-9fd2-b76c7d05626b",
      "user": "0x8480b3aa300dd7fad979d31270732040011a6ca7",
      "sellToken": "0x1e0e030abcb4f07de629dccea458a271e0e82624",
      "buyToken": "0xc1fd14775c8665b31c7154074f537338774351eb",
      "sellAmount": "10000",
      "underlyingAsset": "0x1e0e030abcb4f07de629dccea458a271e0e82624",
      "expiryTime": 1760089073,
      "componentAssets": [],
      "redemptionTime": 1760088473,
      "status": "pending",
      "slippageTolerance": 3,
      "createdAt": "2025-10-06T22:07:53.048Z",
      "updatedAt": "2025-10-06T22:07:53.048Z",
      "timestamp": 1759788473048,
      "metadata": {
        "redeemAsset": "WPLUME",
        "assetAddress": "0x1e0e030abcb4f07de629dccea458a271e0e82624",
        "collateralType": "Digital asset",
        "accessType": "Permissionless",
        "yield": "0",
        "decimals": "18",
        "riskRating": "BBB",
        "exchangeProxy": "0x21722911A85C6F6F1Bb84273B1Aa882C59121179"
        "rfqOrder": {
          "makerToken": "0xc1fd14775c8665b31c7154074f537338774351eb",
          "takerToken": "0x1e0e030abcb4f07de629dccea458a271e0e82624",
          "makerAmount": "",
          "takerAmount": "10000",
          "maker": "",
          "taker": "0x8480b3aa300dd7fad979d31270732040011a6ca7",
          "pool": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "sender": "0x0000000000000000000000000000000000000000",
          "feeRecipient": "0x46892c3265BCe02a17408D86a5120b4D62D7606b",
          "takerTokenFeeAmount": 100,
          "expiry": 1760089073,
          "salt": "6caa1f2f-0743-4470-9fd2-b76c7d05626b"
        }
      }
    },
```

Once you see all the available opportunities, you can start defining a thesis around which ones you want to bid on and which ones you do not. After you know on which RFQ's you want to start participating in, you can now start automatically sending us quotes when you receive relevant RFQ's.

### (Optional) Approve Contract

You will need to approve Mystic's contract so it can take assets from your wallet when you win RFQs. You can either choose to do an unlimited approval of the relevant contract address once before you start bidding, or do it for smaller amounts every time you bid. We advise you to approve it once, as it will make bidding easier for you moving forward.

Here is a code sample of how you can approve contract via code by referencing variables available in [Query Redemption Requests](#query-redemption-requests):

```
import { ethers } from 'ethers';

// Unlimited approval to Exchange Proxy (spender) from rfqRequest.metadata
export async function approveUnlimitedToExchangeProxy(
  rfqRequest: { metadata?: { exchangeProxy?: string; } },
  token: string,
  signer: ethers.Signer,
) {
  const spender =
    rfqRequest?.metadata?.exchangeProxy;
  if (!spender) throw new Error('metadata.exchangeProxy (or allowanceTarget) missing');

  const erc20 = new ethers.Contract(
    token,
    [
      'function approve(address spender, uint256 amount) external returns (bool)',
      'function allowance(address owner, address spender) view returns (uint256)',
      'function decimals() view returns (uint8)',
    ],
    signer,
  );

  const owner = await signer.getAddress();
  const current = await erc20.allowance(owner, spender);
  if (current.gt(0)) return { txHash: null, alreadyApproved: true };

  const tx = await erc20.approve(spender, ethers.constants.MaxUint256);
  const receipt = await tx.wait();
  return { txHash: receipt.transactionHash, alreadyApproved: false };
}
```

### Sign Order

For your quote to be able to fill an RFQ, you need first sign the respective order. You will be signing and sending a quote every time you wish to participate in an RFQ. Here is a code sample of how you sign one:

```
// Use rfqOrder from RFQRequest.metadata to build/sign the final order.
// Only provide makerAmount and maker (everything else comes from metadata.rfqOrder).

import { LimitOrder, SignatureType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { Wallet } from 'ethers';

type RFQOrderFromMetadata = {
  chainId: number,
  verifyingContract: string,
  makerToken: string,
  takerToken: string,
  takerAmount: string,
  taker: string, 
  pool: string,
  sender: string,
  feeRecipient: string,
  takerTokenFeeAmount: string,
  expiry: number,
  salt: number,
};

export async function signFromMetadata(
  rfqRequest: any, 
  makerAmount: string,
  maker: string,
) {
  const base: RFQOrderFromMetadata = rfqRequest?.metadata?.rfqOrder;
  if (!base) throw new Error('metadata.rfqOrder missing');

  const order = new LimitOrder({
    chainId: base.chainId,
    verifyingContract: base.,verifyingContract,
    makerToken: base.makerToken,
    takerToken: base.takerToken,
    makerAmount: new BigNumber(makerAmount),
    takerAmount: new BigNumber(base.takerAmount),
    maker,
    taker: base.taker,
    pool: base.pool,
    sender: base.sender,
    feeRecipient: base.feeRecipient,
    takerTokenFeeAmount: new BigNumber(String(base.takerTokenFeeAmount)),
    expiry: new BigNumber(base.expiry),
    salt: new BigNumber(base.salt),
  });

  const signature = await order.getSignatureWithKey(
    new Wallet(process.env.PRIV_KEY).privateKey,
    SignatureType.EIP712,
  );

  return {
    order: {
      chainId: order.chainId,
      verifyingContract: order.verifyingContract,
      makerToken: order.makerToken,
      takerToken: order.takerToken,
      makerAmount: order.makerAmount.toString(),
      takerAmount: order.takerAmount.toString(),
      maker: order.maker,
      taker: order.taker,
      pool: order.pool,
      sender: order.sender,
      feeRecipient: order.feeRecipient,
      takerTokenFeeAmount: order.takerTokenFeeAmount.toString(),
      expiry: order.expiry.toString(),
      salt: order.salt.toString(),
    },
    signature,
  };
}
```

### Send Quotes

To send us a quote, you need to both sign and send us a response via POST request to the following endpoint:

```
https://api.mysticfinance.xyz/redemption/quote
```

Assuming you have already signed as per the code sample in the previous section, this is the format that the POST request you're sending should be in, which includes the signature above:

```
{
  "requestId": "6387df25-d64b-485b-8ed7-5422459bcf35",
  "maker": "0x8480b3aa300dd7fad979d31270732040011a6ca7",
  "makerAmount": "10000",
  "signature": {
        "signatureType": 2,
        "v": 27,
        "r": "0xd988ae764344e7a116eb1924f491d81736558025ed63ce4f28759a7b68ff58a6",
        "s": "0x1d0d1a92f7087d39e29ecb9416a8105209f19c85d199e2179d6d268395157329"
      },
  "expiry": 1000
}
```

Do note:

* The price you are quoting is calculated as `takerAmount/makerAmount`;
* `maker` is your own address, with which you first registered as a Solver;
* Responses must be sent before the  `expiry`  date in the request for quote (`1760089073` in the example above).

### Winner Selection

When the request for quote hits its expiry date, Mystic selects the bid with the best price and awards it the trade. We then use the winning bid's details to create an RFQ object which will be used to fill the order on-chain. As such, once the RFQ object is created, Mystic fills the trade with the winning bid by bundling settlement and fee distribution. At once, two things happen:

* RFQ order fill - effectively taking the redemption asset from the Solver and giving it to the user and taking the redeem asset from the user and giving it to the Solver.
* Fee settlement - taking the protocol fee and sending the remaining assets to the Solver.

Once this is done, Mystic updates the losing bids to `Rejected` , the winning bid to `Accepted` and the redemption request to `Completed`.


# Structured Vaults (coming soon)

### Introduction

The main problem with making lending markets for RWAs is the lack of secondary market liquidity. This prevents looping and, worst of all in the case of RWAs, prevents liquidations.

To solve for this, Mystic has come up with a novel lending primitive: structured vaults. These vaults, built atop of Morpho's infrastructure, essentially mimic a real-life credit product by creating safety vaults tied to each Morpho vault to act as first-loss capital, thus enabling RWA liquidations.

### What Are Structured Vaults

At their most granular, they are Morpho vaults. However, something has been added to them: each vault is tied to one or multiple vaults called **Safety Vaults**. Built on the ERC-4626 standard, these accept only one token each and act as first-loss capital for the vault: in case of liquidation/bad debt, safety vaults take the loss first to protect the (Morpho) vault. The set of Safety Vaults protecting a single Morpho Vault comprises that vault's **Safety Module**.

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2F2wmZYHKDQQ4jcPpFpgGE%2FSwimlane%20Diagram%20(Community).png?alt=media&amp;token=7da812c6-af88-4e4a-bc5c-c499e70ef16f" alt=""><figcaption></figcaption></figure>

### How it Works

Curators tie a number of safety vaults at their discretion to each of their Morpho vaults, creating effectively a composable **Safety Module** for each of their curated vaults. Since each safety vault can protect more than one Morpho vault, this enables an interesting dynamic - **shared safety**. Curators can now protect each other's vaults or multiple of their own vaults with the same safety vault. Let's look at an example:

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2Fh0Xza2BvJW4eFN4rOwA4%2FSwimlane%20Diagram%20(Community)%20(1).png?alt=media&amp;token=f4d42b1f-eb12-43f7-a794-1755139c0bc0" alt=""><figcaption></figcaption></figure>

In this example, very similar to the above, there are two Morpho vaults, each with their own Safety Module. However, the USDC Safety Vault protects both the Eth Vault and the USDC Vault, effectively bringing shared safety to the markets.

From a more operational perspective, lenders can supply WETH to the Morpho vault (Senior position) and then stake their receipt token in the Safety Module to protect the respective Morpho vault (Junior position). They can also stake directly in the Safety Module to have a direct Junior position.

An important note to make is regarding stakers in the USDC Safety Vault - they get cumulative yield for both Morpho vaults that the vault secures, as in case of liquidation, both get to tap into the vault for first-loss capital. The order in which they may do so is defined below in [realization mechanisms](#realization-mechanisms).

### Vault Compensation

Curators can choose to incentivize the safety vaults with rewards or assign a percentage of the Morpho vault's yield that goes only to safety vault participants.

### Liquidity Extension

In the event that a safety vault has the same asset as the borrow asset of a Morpho vault it is covering, the vault manager can call for a liquidity extension - moving funds from the safety vault to the Morpho vault, so they may be borrowed. This enables the safety vault to effectively act as a Junior Tranche, as its participants may also become lenders. Liquidity extensions need to be enabled per safety vault.

### Liquidations in Structured Vaults

In case of defaults, first liquidation bots active on each market will participate to liquidate the debt. Only in case they do not participate or the asset is illiquid will the Mystic liquidation process trigger. In this case, two things happen sequentially:

* First, Mystic **Dutch auctions** the collateral with whitelisted parties up to the asset's liquidation threshold. If all the debt is solved this way, the liquidation is thereby settled. If not, liquidation then falls to the respective safety vaults.
* If bad debt reaches a safety module, debt is then realized according to each safety vault's realization mechanism. Upon realization, the respective receipt tokens are burned / withdrawn for their respective assets to cover the necessary debt.

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FKpVAd2CWsTxRAMS1vGUU%2FSwimlane%20Diagram%20(Community)%20(2).png?alt=media&amp;token=03ae1f64-addc-436b-a942-ca452e8e52b2" alt=""><figcaption></figcaption></figure>

### Dutch Auction

When there is a liquidation, and assuming there is insufficient DEX liquidity to cover it , it is then up to the auction process to solve it.

Here, multiple parties act as solvers and compete for the liquidation by participating in a dutch auction, placing bids on Mystic's bidding system as the price descends until it hits the LLTV price. Ultimately, the best bid will take the liquidation. The winning solver then fronts the liquidity necessary to settle the market and wins the corresponding collateral plus a liquidation premium as a reward.

This innovative model brings a cushion that enables RWAs to be woven into DeFi effectively for the first time, by predictably bridging their offchain-onchain redemption times and making them significantly more liquid and DeFi-ready.

### Realization Mechanisms

If only one safety vault is associated with a single Morpho vault, then it is immediate that it takes any loss that happens. However, it may happen that one safety vault is covering multiple Morpho Vaults or that one Morpho vault is being covered by multiple safety vaults. In these scenarios, two realization formats are possible:

* Automatic realization: all safety vaults equally share the loss.
* Tiered realization: vaults cover a percentage of the debt proportional to the yield they receive per Morpho vault, meaning highest-paid vaults get liquidated first. Let's look at two examples:
  * Let's say a single Morpho vault is covered by two safety vaults, one of $5M earning 10% APY and one of $10M earning 15% APY (because the vault manager incentivized them differently). In this case, the vault earning 15% gets liquidated first in proportion to its yield difference. The $5M vault is still responsible to cover the debt, only a smaller percentage of it.
  * Let's say a single safety vault is covering two Morpho vaults, and both get liquidated simultaneously. In this case, the market paying more to the vault gets prioritized and is covered first. If both vaults pay the same, then the vault equally covers both.

Vault managers decide which realization mechanism they want to apply to their safety modules. Do note, safety vaults can have Senior-Junior relationships between them, which enables curators to effectively build a sequence of vaults protecting a Morpho vault and decide the order in which they are liquidated.

### Benefits

* Lenders unlock additional utility on their receipt tokens while maintaining total transparency and control.
* RWA liquidations are now possible, and with them, the introduction of RWAs in DeFi.
* Morpho vaults unlock new customizable security systems that combine different assets.
* Curators earn by taking a performance fee on yield generated by their curated vaults.

This whole architecture was built to be extremely flexible and scalable - new vaults can be added permissionlessly according to curator need. In addition, the whole system is extremely capital-efficient and modular, as the same asset can protect more than one vault at the same time and risks can be closely managed according to curator preferences.


# Oracles

Mystic works with two partners for Oracles - Stork and eOracle. More about them follows.

### Stork

Stork specializes in providing low latency price feeds to DeFi applications. It aggregates price data off-chain from exchanges & applications and validates it using cryptographic signatures. The data is then sent to the Plume blockchain, making Stork suitable for reliable, real-time price updates even in volatile market conditions.\
\
**Stork feeds:**\
\- **Ultra-Low Latency:** Provides rapid updates for real-time data accuracy.\
\- **Efficient Data Handling:** Aggregates prices off-chain to reduce latency before transmitting them to the blockchain.\
\- **Wide Data Coverage:** Sources from top exchanges and native Plume applications to ensure broad asset coverage.\
\
**Stork Plume Contract Addresses:**\
<https://docs.stork.network/resources/contract-addresses/evm#plume>

### eOracle

eOracle is the secure infrastructure powering real-time data feeds. Built by risk management and cybersecurity experts, backed by $8B of staked security and validated by 140 validators worldwide - eOracle is the most secure oracle network.\
\
**RWAfi Feeds:**\
eOracle provides secure and verifiable price feeds for tokenized off-chain financial instruments, including treasuries, commodities, private credit, and others, enabling robust RWAfi markets.\
\
eOracle ensures accurate onchain valuations, facilitating the safe use of RWAs as collateral across DeFi lending protocols. By bridging TradFi data with decentralized systems, eOracle addresses core risk parameters such as pricing integrity, latency, and oracle reliability, unlocking the full potential of RWA-backed capital markets.\
\
**eOracle Plume Contract Addresses:**\
<https://docs.eo.app/docs/eprice/feeds-addresses/price-feed-addresses/plume>


# Liquid Staking - myPLUME

## What Is myPLUME

myPLUME is the premier liquid staking product for the PLUME token, built on Frax's frxETH and Plume staking contracts. It uses a one-token system, myPLUME, which gives users the ability to tap into DeFi while also earning staking rewards.

## How It Works

When holding PLUME, users can deposit it to mint myPLUME, a yield-bearing asset loosely pegged to PLUME. As long as they're holding it, users will be accruing staking rewards, which they can then claim in the "Claim" section of Mystic. This allows you to earn staking rewards without running your own validator, while also using your myPLUME tokens in DeFi.

## About myPLUME

In practice, when you stake PLUME to get myPLUME, your tokens are given to Mystic's choice validator. The respective staking rewards you get from staking, given in PLUME, are kept in the Plume Staking Contract, which we then retrieve and send to users when they claim.

The myPLUME peg is defined as 1% of the exchange rate on each side - 1.01 to .9900.

To ensure fairness, reward distribution is done linearly. This means that rewards are attributed according to the time each user has spent holding myPLUME.

### The Buffer

Most PLUME deposited on Mystic is staked on the Plume blockchain. However, a small amount (2%) is kept as a safety fund / buffer instead of being staked. This both facilitates faster withdrawals and overcollateralizes myPLUME to cover slashing events/unforeseen penalties.

### Withdrawing myPLUME

MyPLUME can be withdrawn directly for PLUME in one of three ways:

* **Via Mystic's PLUME Buffer** - if what you are withdrawing is within buffer capacity, you can always withdraw your myPLUME for PLUME instantly.
* **Via AMMs** - swap your myPLUME for PLUME directly on an ecosystem DEX such as [Rooster Protocol](https://www.rooster-protocol.xyz/) or [Ambient Finance](https://ambient.finance/).
* **Via validator unstaking** - you first unstake from the validator with Mystic and, after a time buffer, you can then withdraw your holdings. This time buffer safeguards you against frontrunning, MEV, and other arbitrage externalities.

### Fees

myPLUME fees are as follows:

* Minting = 0%
* Yield Fee = 10%
* Validator Unstaking Withdrawal = 0.03%
* Instant Withdrawal (through Buffer) = 0.15%


# Leverage Strategies

## What is leverage on Mystic

Leverage is a product that enables you to 1-click leverage your exposure to assets on Plume, and has two major use cases:

* **Leverage yield exposure** - by leveraging and increasing your holdings on a yield-bearing asset, you increase your total yield.
* **Long/short an asset** - by leveraging and increasing your position on an asset whose price we think may appreciate (e.g. PLUME), you are extra exposed to price fluctuations and will profit (or take a loss) accordingly.

Mystic's leverage product integrates our 1-click leverage smart contracts with Mystic's vaults, so that any asset on Mystic can be easily leveraged. Let's look at how it works.

## How it works

The leverage implemented on Mystic is flash leverage, meaning we are increasing your holdings of an asset by contracting an equivalent debt via a flash loan. Let's look at how it works overall and then break it down step-by-step.

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FZT2r24YAQmASBoixjLGM%2FUser%20(1).jpg?alt=media&amp;token=744ae88c-dfd8-4db5-8370-03f94c273953" alt=""><figcaption></figcaption></figure>

* First, you select the asset you want to deposit and the desired leverage multiplier you want. The multiplier figure expresses how many times you are gaining additional exposure to the asset. In this example, let's imagine we want to 4x leverage nRWA starting with a 100 nRWA position.

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FGLF50B6YhU1HJAMfBAvc%2Flevearge232.png?alt=media&amp;token=0872de34-e3ea-4701-a182-5d069127dd6b" alt=""><figcaption></figcaption></figure>

* After you deposit your 100 nRWA and choose your multiplier, Mystic flash borrows the remaining amount necessary to meet your desired leverage. In this example, Mystic will flash borrow 300 pUSD (to meet the 4x leverage). Had you started with pUSD, Mystic would have first swapped it for nRWA before proceeding with the flash borrow.
* Next, Mystic goes to a DEX (our partner [Rooster](https://www.rooster-protocol.xyz/) in this case) and swaps that 300 pUSD into nRWA.
* We then deposit the 300 nRWA into Mystic and take out a 300 pUSD loan, to repay the flash loan executed beforehand.

The end result of this is a 400 nRWA deposit and a 300 pUSD debt to the protocol, meaning you are now gaining the yield of the 400 nRWA instead of just the yield you could have achieved with your initial 100 nRWA.

## Why leverage strategies

Leverage is just the beginning here, as we intend to roll out more strategies in the future. There reason for this is two-fold: liquidity sharing and convenience. On one hand, liquidity is DeFi's main bottleneck and also its living blood. Enabling custom strategies to be built on top of Mystic means third-party protocols can now tap into Mystic's liquidity by creating their own strategies, without having to bootstrap liquidity or raise via backroom TVL deals. This will not only dramatically accelerate both Mystic's supply and borrow activity, but directly help solve one of DeFi's biggest issues - the cold-start problem.

In addition to this, convenience is a big factor. Leverage and other complex trades can often be quite cumbersome to do and manage manually. To scale DeFi on Plume, we think it is essential these strategies can be performed and managed seamlessly, ideally with just 1 click. That is exactly what Mystic enables with leverage - an automated, user-friendly way to achieve leverage, the DeFi-native way.


# FAQ

## What is Mystic?

Mystic is a lending market for tokenized real-world assets, which focuses on leveraging the best of RWAs to maximize their use in DeFi. The protocol is composed of curated lending vaults, 1-click looping and the ecosystem's key LST, myPLUME.

## Why Mystic?

The mechanisms of TradFi are inefficient and slow, both putting people at risk due to excessive counterparty risk and locking many out of the system due to often misplaced operational and regulatory concerns. In comparison, crypto lending is much cheaper, faster, more secure and transparent for consumers.

It stood out to us that, if we are to bring DeFi lending to the world, we need to have a lending market structured so that off-chain assets can be seamlessly supported, and where borrowers, lenders and underwriters can seamlessly connect with one another. That is what we are building at Mystic.

## How is it different than other lending markets?

Current lending markets lack the modularity and support for RWAs to be able to deliver on the above vision. Mystic steps in as an ecosystem where that is not only possible, but which has been purpose-built for this mission.

## How do I interact with Mystic?

You can lend and borrow directly via our interface [here](https://app.mysticfinance.xyz/) or access our website at [https://mysticfinance.xyz.](https://mysticfinance.xyz) To see how to borrow and supply on Mystic, please see our [User Guide](/user-guide/supply).

## How has the protocol been audited for security?

Mystic has been built on top of Morpho, which has been extensively audited and battle-tested. Its audits may be found [here](https://docs.morpho.org/getting-started/resources/audits/). Mystic’s additional changes of protocol permissioning, custodian integrations and our LST have been thoroughly audited by the best in the business also. See our audits [here](https://hacken.io/audits/mystic-finance/).

## What are the fees?

* A 5-20% interest rate fee on lending markets split by Mystic and our curators.
* 10% reward fee on our LST.

## What is the health factor?

The health factor is the representation of how much collateral is backing your debt positions, which in turn expresses how close it is to liquidation. It is calculated as:

$$
\frac{Total Collateral Value \* LiquidationLTV}{TotalBorrowValue}
$$

Liquidation LTV (LLTV) is the point at which a position becomes eligible for liquidation. It is usually slightly higher than the market’s LTV, to give borrowers some room to avoid liquidations. As for how to interpret the HF:

* If HF < 1, then the position is eligible for liquidation.
* If HF >1, then the position is considered healthy.

## **How do liquidations work on Mystic?**

A position is eligible for liquidation when its health factor is equal to or below 1, meaning it has reached the market’s LLTV. When this happens, liquidators repay up to 100% of the position’s debt for a premium by using one the 3 liquidation processes described in [liquidations](/core-concepts/liquidations).

## Are flash loans available?

Flash loans were first pioneered by Aave, and they let users borrow any amount without collateral, so long as they repay their loan in the same transaction. This is typically used when doing liquidations, leveraging a position or doing arbitrage.

You can do a flash loan by calling the flashLoan function in our smart contracts. Please note the caller contract must have a callback function called executeOperation which returns the amount loaned.

## How do vaults work?

Vaults send their liquidity to sets of pools that all contain the same one borrow asset (e.g. USDC). This way, vaults enable creating custom lending value propositions that are more attractive to lenders.


# Mystic Morpho REST API

Read real-time Morpho data on the chains Mystic is on, including vaults and markets data, APYs, TVL, liquidity, rewards and much more.

### Introduction

The Mystic Morpho API lets you read all kinds of Morpho data in real-time on the chains Morpho is on. Track your positions, liquidate, manage risk or build your own Morpho-powered app using Mystic's Morpho API. Note that to use it, you will need your own API Key. Please contact us to get an API Key and get started -> [**Contact us**](mailto:joao.moreira@mysticfinance.xyz)**.**

### Base URL

{% tabs %}
{% tab title="Production" %}

```
https://api.mysticfinance.xyz
```

{% endtab %}

{% tab title="Staging" %}

```
https://staging-api.mysticfinance.xyz
```

{% endtab %}
{% endtabs %}

### OpenAPI Specification

The full machine-readable API spec available for AI agents, code generators, and API clients:

| Resource     | Url                                                                                       |
| ------------ | ----------------------------------------------------------------------------------------- |
| Swagger UI   | <https://api.mysticfinance.xyz/docs>                                                      |
| OpenAPI JSON | [https://api.mysticfinance.xyz/docs-json](<https://api.mysticfinance.xyz/docs-json&#xA;>) |

### Contents

* Why Mystic
* Authentication
* Quickstart
* API reference
  * General
  * Vaults
  * Borrows
  * Portfolio
  * Transactions
  * Historical
  * Realtime
  * Liquidations
* Supported protocols and chains
* Errors

### Why Mystic

Use one API for DeFi earn and borrow data, risk context, eligibility screening, portfolio tracking, position history, and live on-chain state.

Mystic is the API layer for onchain earn and borrow features. Use one integration to discover vaults, compare risk and return, screen out markets that should never reach a user, size a borrow, track positions and returns after a deposit, and see what happened when capital move  across 1.5K+ vaults, 5.5k+ borrow markets, and 6+ chains.

Use Mystic when your product needs to do more than show APYs, to decide which vaults and markets are eligible, explain why one beats another, tell a user what they hold and what it has earned, and report what happened after capital moved, including the liquidations.&#x20;

### Authentication

To call the API, you need to an API Key. Contact the Mystic team to get one and add it to the header of the api call like below. API Key usage is billed monthly and the Key will expire if payment fails. An expired key returns `401`.&#x20;

```
curl "https://api.mysticfinance.xyz/morphoCache/lite?chainId=1" \
  -H "x-api-key: YOUR_API_KEY"
```

\
**Rate limit:** 20 - 100 requests/second on data endpoints depending on your API plan. When you are over the limit, it returns `429`

[**Contact us to get your API Key**](mailto:joao.moreira@mysticfinance.xyz)<br>

### Quickstart

Read a vault and a user's position in it, in 5 steps:

1. Fetch the list of vaults
2. Fetch a vault's details
3. Fetch a vault's allocations
4. Check a user's position in that vault
5. Check the whole portfolio

Read a borrow market and a user's position in it, in 6 steps:

1. Fetch the list of borrow markets
2. Fetch a market's details
3. Fetch a market's allocations
4. Check a user's borrow positions
5. Narrow to one market
6. Check the whole portfolio

**Two conventions before you start:**

**Rates are decimal fractions, not percentages.** `0.0752` means 7.52%. This holds for every APY, LLTV, utilization, fee and liquidation penalty in every request and response, `minApy=0.05` filters at 5%, not 5 basis points.

**Amounts are decimal strings, not numbers.** `"12403118.44"`, already scaled by the token's decimals.&#x20;

All examples use JavaScript with axios for HTTP.

```js
import axios from 'axios';

const mystic = axios.create({
  baseURL: 'https://api.mysticfinance.xyz/v1/yields',
  headers: { 'x-api-key': process.env.MYSTIC_API_KEY },
});

/** "plume:0x0b14…" → ["plume", "0x0b14…"] */
const splitId = (id) => id.split(':');
```

#### Vaults

**1. Fetch the list of vaults**

Filter and rank across every supported network in one call. This is the endpoint a "where should I deposit?" screen is built on:

```js
async function listVaults(params) {
  const { data } = await mystic.get('/vaults', { params });
  return data;
}

const page = await listVaults({
  assetGroup: 'usd',
  minTvl: 1_000_000,
  minApy: 0.05,          // 5%, not 5%%
  sortBy: 'apy',
  perPage: 10,
});
```

Example response (`GET /vaults?assetGroup=usd&minTvl=1000000&minApy=0.05&sortBy=apy&perPage=10`):

```jsonc
{
  "data": [{
    "vaultId": "plume:0x0b14d0bdaf647c541d3887c5b1a4bd64068fcda7",
    "address": "0x0b14d0bdaf647c541d3887c5b1a4bd64068fcda7",
    "name": "Mystic MEV Capital pUSD",
    "network":  { "name": "Plume", "slug": "plume", "chainId": 98866, "networkCaip": "eip155:98866" },
    "protocol": { "name": "morpho", "displayName": "Morpho", "product": "vault", "version": "v1.1" },
    "asset": { "address": "0xdddd…", "symbol": "pUSD", "decimals": 6, "priceUsd": "1.0002", "assetGroup": "usd" },
    "apy": {
      "instantaneous": { "base": 0.0642, "reward": 0.0110, "total": 0.0752 },
      "1day":  { "base": 0.0612, "reward": 0.0140, "total": 0.0752 },
      "7day":  { "base": 0.0591, "reward": 0.0140, "total": 0.0731 },
      "30day": { "base": 0.0575, "reward": 0.0138, "total": 0.0713 }
    },
    "tvl":       { "native": "12397600.0", "usd": "12400079.5" },
    "liquidity": { "native": "3120004.1",  "usd": "3120628.1" },
    "curator": { "name": "MEV", "logoUri": "https://…" },
    "tags": ["stablecoin", "blue-chip", "incentivised"],
    "score": { "vaultScore": 78 },
    "holderCount": 137,
    "lastUpdateTimestamp": 1753747200
  }],
  "pagination": { "page": 1, "perPage": 10, "total": 34, "totalPages": 4, "hasNextPage": true },
  "errors": { "unsupportedNetworks": [], "unsupportedAssets": [], "unsupportedProtocols": [] }
}
```

Read `apy.instantaneous.reward` against `apy["30day"].reward` before you render a headline rate.&#x20;

**2. Fetch a vault's details**

Split the `vaultId` and read the vault in full:

```js
async function getVault(vaultId) {
  const [network, address] = splitId(vaultId);
  const { data } = await mystic.get(`/vaults/${network}/${address}`);
  return data;
}
```

The detail response is the same object as the list plus `allocations`, `governance`, `collaterals`, `capacity`, `protocolSpecific` and the full score breakdown, see the vault object for every field.

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "name": "Mystic MEV Capital pUSD",
  "fees": { "performanceFee": 0.1, "managementFee": null, "withdrawalFee": null, "depositFee": null },
  "capacity": { "maxCapacity": "50000000.0", "remainingCapacity": "37602400.0" },
  "sharePrice": { "value": "1.041233", "asset": "pUSD" },
  "curators": [{ "name": "MEV" }, { "name": "Cicada" }],
  "governance": { "owner": "0x…", "curator": "0x…", "guardian": null, "timelockSeconds": 86400 },
  "score": { "vaultScore": 78, "vaultMaturityScore": 90, "allocationQualityScore": 72, "totalScorePenalty": 0 }
  // … plus everything from the list row
}
```

A missing vault is a `404`, never a `200` with a null body, so a failed lookup throws `404` in axios.&#x20;

**3. Fetch a vault's allocations**

Where the deposits actually go, the answer to "what risk am I taking?":

```js
async function getAllocations(vaultId) {
  const [network, address] = splitId(vaultId);
  const { data } = await mystic.get(`/vaults/${network}/${address}/allocations`);
  return data;
}
```

Example response, largest share first:

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "asset": { "symbol": "pUSD", "address": "0xdddd…", "priceUsd": "1.0002", "assetGroup": "usd" },
  "data": [
    { "kind": "market", "share": 0.62, "amount": { "native": "7686512.0", "usd": "7688049.3" },
      "market": { "marketId": "plume:0x7a96549c…", "collateralAsset": { "symbol": "nALPHA" }, "lltv": 0.86,
                  "utilization": 0.8794, "apy": { "supply": { "total": 0.0373 }, "borrow": { "total": 0.0426 } } },
      "position": { "supplyShares": "7301220.4", "borrowShares": null, "collateral": null }, "vault": null },
    { "kind": "vault",  "share": 0.28, "amount": { "native": "3471328.0", "usd": "3472022.2" },
      "vault": { "vaultId": "plume:0xc0df…", "name": "Re7 RWA Yield", "version": "v1.1", "asset": "pUSD" } },
    { "kind": "idle",   "share": 0.10, "amount": { "native": "1239760.0", "usd": "1240008.0" } }
  ],
  "lastUpdateTimestamp": 1753747200
}
```

**4. Check a user's position in that vault**

```js
async function getPosition(address, vaultId) {
  const [network, vault] = splitId(vaultId);
  const { data } = await mystic.get(`/portfolio/positions/${address}/${network}/${vault}`);
  return data;
}
```

Example response:

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "name": "Mystic MEV Capital pUSD",
  "network": { "slug": "plume", "chainId": 98866 },
  "asset": { "symbol": "pUSD", "decimals": 6, "priceUsd": "1.0002" },
  "balance": { "shares": "119843.22", "native": "124776.5", "usd": "124801.4" },
  "apy": { "base": 0.0612, "reward": 0.0140, "total": 0.0752 },
  "projectedUsdAnnualEarnings": "9384.9"
}
```

**5. Check the whole portfolio**

One call for every total across every indexed network:

```js
async function getSummary(address) {
  const { data } = await mystic.get(`/portfolio/summary/${address}`);
  return data;
}

async function getPositions(address) {
  const { data } = await mystic.get(`/portfolio/positions/${address}`);
  return data;   // list envelope of the position objects from step 4
}
```

Example response (`GET /portfolio/summary/0xd8dA…6045`):

```jsonc
{
  "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  "totals": { "suppliedUsd": "124000.5", "collateralUsd": "50000", "debtUsd": "20000", "netWorthUsd": "154000.5" },
  "weightedApy": 0.0641,
  "projectedUsdAnnualEarnings": "7948.4",
  "positionCount": 4,
  "borrowPositionCount": 1,
  "lowestHealthFactor": 2.15,
  "networks": ["plume", "flare"],
  "errors": { "unsupportedNetworks": [], "unsupportedAssets": [], "unsupportedProtocols": [] }
}
```

**Putting the vault flow together**

```js
async function vaultFlow(user) {
  // 1. discover
  const { data: vaults } = await mystic.get('/vaults', {
    params: { assetGroup: 'usd', minTvl: 1_000_000, sortBy: 'apy', perPage: 10 },
  });
  const best = vaults[0];                       // already sorted; [] means nothing matched

  // 2 + 3. detail and where it deploys
  const [network, address] = splitId(best.vaultId);
  const [vault, allocations] = await Promise.all([
    mystic.get(`/vaults/${network}/${address}`).then((r) => r.data),
    mystic.get(`/vaults/${network}/${address}/allocations`).then((r) => r.data),
  ]);
  const idle = allocations.data.find((a) => a.kind === 'idle')?.share ?? 0;

  // 4 + 5. the user's side
  const [position, summary] = await Promise.all([
    mystic.get(`/portfolio/positions/${user}/${network}/${address}`)
      .then((r) => r.data)
      .catch((e) => (e.response?.status === 404 ? null : Promise.reject(e))),
    mystic.get(`/portfolio/summary/${user}`).then((r) => r.data),
  ]);

  return { vault, idle, position, summary };
}
```

#### Borrows

**1. Fetch the list of borrow markets**

```js
async function listMarkets(params) {
  const { data } = await mystic.get('/borrow/markets', { params });
  return data;
}

const page = await listMarkets({
  allowedBorrowAssets: 'pUSD',   // what the user wants to borrow
  hasLiquidity: true,
  sortBy: 'lltv',
  sortOrder: 'asc',              // safest first
});
```

Example response:

```jsonc
{
  "data": [{
    "marketId": "plume:0x7a96549cae736c913d12c78ee4c155c2d2f874031fce5acdd07bdbf23d7644c7",
    "marketKey": "0x7a96549cae736c913d12c78ee4c155c2d2f874031fce5acdd07bdbf23d7644c7",
    "network": { "name": "Plume", "slug": "plume", "chainId": 98866, "networkCaip": "eip155:98866" },
    "loanAsset":       { "symbol": "pUSD",   "address": "0xdddd…", "priceUsd": "1.0002", "assetGroup": "usd" },
    "collateralAsset": { "symbol": "nALPHA", "address": "0x593c…", "priceUsd": "1.1059", "assetGroup": "rwa" },
    "lltv": 0.86,
    "liquidationThreshold": 0.86,
    "liquidationPenalty": 0.0438,
    "apy": {
      "supply": { "base": 0.0373, "reward": 0, "total": 0.0373 },
      "borrow": { "base": 0.0426, "reward": 0, "total": 0.0426 }
    },
    "supply": { "assets": "4261152.37", "shares": "4048331.17", "usd": "4262004.6" },
    "borrow": { "assets": "3747270.67", "shares": "3535370.85", "usd": "3748020.1" },
    "liquidity": { "native": "513881.7", "usd": "513984.5" },
    "utilization": 0.8794,
    "isIdle": false,
    "score": { "marketScore": 62 }
  }],
  "pagination": { "page": 1, "perPage": 50, "total": 12, "totalPages": 1, "hasNextPage": false },
  "errors": { "unsupportedNetworks": [], "unsupportedAssets": [], "unsupportedProtocols": [] }
}
```

Bad-debt and unverified markets are excluded by default.  Pass `includeBadDebt=true` / `includeUnverified=true` for the unfiltered set; both are also reported in `tags` and `flags` when present.

`supplyingVaults` is omitted from list rows to keep them small. Add `?expand=supplyingVaults` when you need it in a list, an unrecognised `expand` value is a `400`, not a silently slim response.

**2. Fetch a market's details**

```js
async function getMarket(marketId) {
  const [network, key] = splitId(marketId);
  const { data } = await mystic.get(`/borrow/markets/${network}/${key}`);
  return data;
}
```

The detail response adds `reallocatableLiquidity`, `capacity`, `oracle`, `irm`, `fee`, `performanceFee`, `vaultCaps` and the fully-expanded `supplyingVaults,` see the market object.

```jsonc
{
  "marketId": "plume:0x7a96549c…",
  "liquidity":              { "native": "513881.7", "usd": "513984.5" },
  "reallocatableLiquidity": { "native": "109354.5", "usd": "109376.3" },
  "capacity": { "supplyCap": "10000000.0", "totalSupplyCaps": "11000000.0" },
  "oracle": { "address": "0x7824…", "baseFeed": null },
  "irm":    { "address": "0x7420…" },
  "fee": 0,
  "performanceFee": 0.1,
  "lastAccrualTimestamp": 1785516494
}
```

`liquidity` is everything reachable; `reallocatableLiquidity` is the slice that only arrives if a public allocator acts. Borrowing against the first number and assuming the second is instant is how a large draw fails at signing time.

**3. Fetch a market's allocations**

Which vaults back the market, and how much each has in it, depth is only as real as the vaults supplying it:

```js
async function getMarketAllocations(marketId) {
  const [network, key] = splitId(marketId);
  const { data } = await mystic.get(`/borrow/markets/${network}/${key}/allocations`);
  return data;
}
```

```jsonc
{
  "marketId": "plume:0x7a96549c…",
  "data": [{
    "vaultId": "plume:0xc0df…",
    "name": "Re7 RWA Yield",
    "symbol": "Re7pUSD",
    "curators": [{ "name": "Re7", "address": "0x…" }],
    "supplyShare": 0.9999,
    "supplied": { "native": "4048324.07", "usd": "4049133.7" },
    "cap": "10000000.0",
    "allocated": "3906454.7",
    "isActive": true,
    "allocatorFee": 0
  }],
  "lastUpdateTimestamp": 1785516534
}
```

**4. Check a user's borrow positions**

```js
async function getBorrowPositions(address, params) {
  const { data } = await mystic.get(`/portfolio/borrow-positions/${address}`, { params });
  return data;
}
```

Example response:

```jsonc
{
  "data": [{
    "marketId": "plume:0x7a96549c…",
    "network": { "slug": "plume", "chainId": 98866 },
    "collateralAsset": { "symbol": "nALPHA", "address": "0x593c…", "decimals": 6, "priceUsd": "1.1059" },
    "debtAsset":       { "symbol": "pUSD",   "address": "0xdddd…", "decimals": 6, "priceUsd": "1.0002" },
    "collateral": { "native": "10000.0", "usd": "11059.9" },
    "debt":       { "native": "6000.0",  "usd": "6001.2" },
    "lltv": 0.86,
    "ltv": 0.5426,
    "healthFactor": 1.585
  }],
  "pagination": { "page": 1, "perPage": 50, "total": 1, "totalPages": 1, "hasNextPage": false },
  "errors": { "unsupportedNetworks": [], "unsupportedAssets": [], "unsupportedProtocols": [] }
}
```

`healthFactor` is collateral × liquidation threshold ÷ debt, and is `null` when there is no debt, a position that cannot be liquidated has no health factor. Do not treat `null` as zero: it is the safest possible state, not the most dangerous one.

**5. Narrow to one market**

There is no per-market position route, borrow positions come back for the whole wallet, so filter the response by `marketId`:

```js
async function getMarketPosition(address, marketId) {
  const page = await getBorrowPositions(address);
  return page.data.find((p) => p.marketId === marketId) ?? null;
}
```

Scope the fan-out with `allowedNetworks` when you already know the chain — it is one indexer round-trip per network, so naming the one you need is faster and no more expensive:

```js
const page = await getBorrowPositions(user, { allowedNetworks: 'plume' });
```

To see how a position got where it is, every borrow, repayment and liquidation against it — read `/portfolio/events/{address}` with `eventType=borrow,repay,liquidation`.

**6. Check the whole portfolio**

The same two calls as the vault flow. For a borrower, three fields carry the answer:

```js
const summary = await getSummary(user);

summary.totals.debtUsd;        // "20000"
summary.lowestHealthFactor;    // 2.15 — the riskiest position across every market
summary.borrowPositionCount;   // 1
```

`lowestHealthFactor` is what an alerting system should watch. It is `null` only when the wallet has no debt anywhere.

**Putting the borrow flow together**

```js
async function borrowFlow(user, wants = 'pUSD') {
  // 1. find markets that will lend the asset, safest first
  const { data: markets } = await mystic.get('/borrow/markets', {
    params: { allowedBorrowAssets: wants, hasLiquidity: true, sortBy: 'lltv', sortOrder: 'asc' },
  });

  // 2 + 3. detail, and who is actually backing it
  const [network, key] = splitId(markets[0].marketId);
  const [market, allocations] = await Promise.all([
    mystic.get(`/borrow/markets/${network}/${key}`).then((r) => r.data),
    mystic.get(`/borrow/markets/${network}/${key}/allocations`).then((r) => r.data),
  ]);

  // what can actually be drawn right now, vs. what needs an allocator to act
  const now = Number(market.liquidity.native) - Number(market.reallocatableLiquidity?.native ?? 0);
  const concentrated = allocations.data.some((v) => (v.supplyShare ?? 0) > 0.9);

  // 4 + 5 + 6. the user's side
  const positions = await mystic
    .get(`/portfolio/borrow-positions/${user}`, { params: { allowedNetworks: network } })
    .then((r) => r.data);
  const here = positions.data.find((p) => p.marketId === market.marketId) ?? null;
  const summary = await mystic.get(`/portfolio/summary/${user}`).then((r) => r.data);

  return { market, availableNow: now, concentrated, position: here, summary };
}
```

### API reference

Every route below is prefixed with `https://api.mysticfinance.xyz/v1/yields` and all are `GET` endpoints.

#### The list envelope

Every list response, without exception:

```jsonc
{
  "data": [ /* … */ ],
  "pagination": { "page": 1, "perPage": 50, "total": 412, "totalPages": 9, "hasNextPage": true },
  "errors": { "unsupportedNetworks": [], "unsupportedAssets": [], "unsupportedProtocols": [] }
}
```

`errors` is a **degradation, not a failure**. Ask for `allowedNetworks=plume,atlantis` and you get every Plume vault plus `unsupportedNetworks: ["atlantis"]` , rather than a `400` that discards the results you could have had.

#### Shared query parameters

Every list endpoint accepts these. Endpoint sections document only what they add.

| Field                | Type    | Required | Description                                                                                             |
| -------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `page`               | integer | –        | Default `1`.                                                                                            |
| `perPage`            | integer | –        | Default `50`, max `500`.                                                                                |
| `allowedNetworks`    | string  | –        | Slugs or chain ids: `plume,base` or `98866,8453`. Unknown entries land in `errors.unsupportedNetworks`. |
| `disallowedNetworks` | string  | –        | Same format, excluded instead.                                                                          |
| `chainId`            | integer | –        | Single-network alias for callers migrating from `/morphoCache`.                                         |
| `search`             | string  | –        | Free-text match on name, symbol or address.                                                             |
| `sortOrder`          | enum    | –        | `asc` \| `desc`. Default `desc`.                                                                        |

`/v1` is required on every route below.

### General

| Route                         | Credits | Description                                 |
| ----------------------------- | ------- | ------------------------------------------- |
| `/health`                     | 0       | Per-network cache age                       |
| `/networks`                   | 0       | Supported networks with vault/market counts |
| `/protocols`                  | 0       | Protocols and their declared capabilities   |
| `/tags`                       | 0       | Tag vocabulary with live counts             |
| `/curators`                   | 2       | Curators with vault counts and TVL          |
| `/assets`                     | 2       | Every asset used by a vault or market       |
| `/assets/{network}/{address}` | 2       | One asset, with where it is used            |

#### `GET /v1/yields/health`

Freshness per chain, so you can decide what staleness you tolerate rather than trusting a bare `200`. Exempt from rate limiting.

**Request** — no parameters.

**Response**

```jsonc
{
  "status": "ok",
  "networks": [
    { "slug": "ethereum", "chainId": 1, "vaultsAgeSeconds": 84, "marketsAgeSeconds": 120 },
    { "slug": "plume", "chainId": 98866, "vaultsAgeSeconds": 41, "marketsAgeSeconds": 41 }
  ],
  "protocols": ["morpho"]
}
```

`null` for an age means that cache has never been written for that chain.

#### `GET /v1/yields/networks`

**Request** — no parameters.

**Response**

```jsonc
{ "data": [{
  "name": "Plume", "slug": "plume", "chainId": 98866,
  "networkCaip": "eip155:98866",
  "explorerUrl": "https://explorer.plume.org",
  "vaultCount": 13, "marketCount": 43,
  "isSupported": true
}] }
```

A supported network with zero counts is a real state (a chain whose cron has not populated yet), reported rather than hidden.

#### `GET /v1/yields/protocols`

Capabilities are **advertised, not implied** — you can see that a protocol cannot answer share-price history instead of discovering it as an empty series.

**Request:** no parameters.

**Response**

```jsonc
{ "data": [{
  "name": "morpho", "displayName": "Morpho", "product": "vault", "version": "v1.1",
  "protocolUrl": "https://morpho.org", "logoUri": "https://…",
  "capabilities": {
    "vaults": true, "borrowMarkets": true, "events": true,
    "sharePriceHistory": true, "holders": true, "positions": true
  },
  "supportedNetworks": ["ethereum", "base", "plume", "flare", "citrea", "berachain"]
}] }
```

#### `GET /v1/yields/tags`

Counts come from the same derivation the vault list uses, so `?tags=stablecoin` returns the number shown here.

**Response**

```jsonc
{ "data": [
  { "tag": "stablecoin", "description": "Denominated in a US-dollar stablecoin.", "count": 61 },
  { "tag": "blue-chip", "description": "Over $10M TVL.", "count": 12 },
  { "tag": "incentivised", "description": "Carries an active reward campaign on top of the base rate.", "count": 27 }
] }
```

Full vocabulary: `stablecoin`, `eth`, `btc`, `rwa`, `v2`, `incentivised`, `blue-chip`, `bad-debt`, `unverified`.

#### `GET /v1/yields/curators`

**Request** — shared parameters only.

**Response** — list envelope, highest TVL first.

```jsonc
{ "data": [{
  "name": "Re7", "vaultCount": 4, "tvlUsd": "48210553.2", "networks": ["plume", "base"]
}] }
```

Curators named `Unknown` are omitted rather than aggregated into a fake entity.

#### `GET /v1/yields/assets`

Every asset that appears in a vault or market we index — derived, not curated. An asset outside that set has nowhere to be deposited, so listing it would be noise.

**Request**

| Field        | Type | Required | Description                                 |
| ------------ | ---- | -------- | ------------------------------------------- |
| `assetGroup` | csv  | –        | `usd` \| `eth` \| `btc` \| `rwa` \| `other` |
|              |      |          | Plus the shared parameters.                 |

**Response** — list envelope, most-used first.

```jsonc
{ "data": [{
  "address": "0xdddd73f5df1f0dc31373357beac77545dc5a6f3f",
  "symbol": "pUSD", "priceUsd": "1.0002", "assetGroup": "usd",
  "assetCaip": "eip155:98866/erc20:0xdddd…",
  "network": { "name": "Plume", "slug": "plume", "chainId": 98866, "networkCaip": "eip155:98866" },
  "vaultCount": 6, "marketCount": 21
}] }
```

The same symbol on two chains is two entries, different tokens, different prices.

#### `GET /v1/yields/assets/{network}/{address}`

**Request**

| Field     | Type | Required | Description             |
| --------- | ---- | -------- | ----------------------- |
| `network` | path | ✅        | Network slug.           |
| `address` | path | ✅        | Asset contract address. |

**Response:** one asset object as above. `404` when the asset is not used by anything we index.

***

### Vaults

| Route                                     | Credits | Description                               |
| ----------------------------------------- | ------- | ----------------------------------------- |
| `/vaults`                                 | 1       | List vaults — the main discovery endpoint |
| `/vaults/{network}/{vaultId}`             | 1       | One vault, in full                        |
| `/vaults/{network}/{vaultId}/apy`         | 1       | APY breakdown alone — a cheap poll        |
| `/vaults/{network}/{vaultId}/tvl`         | 1       | TVL and liquidity alone                   |
| `/vaults/{network}/{vaultId}/allocations` | 1       | Where the vault deploys its assets        |
| `/vaults/{network}/{vaultId}/campaigns`   | 2       | Reward campaigns: token, budget, end date |
| `/vaults/{network}/{vaultId}/holders`     | 2       | Holder count and largest positions        |

#### `GET /v1/yields/vaults`

Defaults to **all** networks, an omitted network filter is not silently treated as Plume.

**Request**

| Field                                      | Type   | Required | Description                                                                                                    |
| ------------------------------------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------- |
| `sortBy`                                   | enum   | –        | `tvl` (default), `apy`, `liquidity`, `name`, `score`, `createdAt`, `holders`                                   |
| `view`                                     | enum   | –        | `full` (default) \| `slim` — see below                                                                         |
| `expand`                                   | string | –        | `score`. Adds the component breakdown a list otherwise omits. An unrecognised value is a `400`.                |
| `allowedAssets` / `disallowedAssets`       | string | –        | Vault assets, by symbol or address                                                                             |
| `assetGroup`                               | string | –        | `usd` \| `eth` \| `btc` \| `rwa` \| `other`                                                                    |
| `allowedProtocols` / `disallowedProtocols` | string | –        | See `/protocols`                                                                                               |
| `curators`                                 | string | –        | Names or addresses                                                                                             |
| `tags`                                     | string | –        | See `/tags`                                                                                                    |
| `minTvl` / `maxTvl`                        | number | –        | USD                                                                                                            |
| `minApy` / `maxApy`                        | number | –        | **Fractions.** `0.05` is 5%.                                                                                   |
| `apyInterval`                              | enum   | –        | `instantaneous`, `1hour`, `1day` (default), `7day`, `30day` — which window `minApy`/`maxApy`/`sortBy=apy` read |
| `minLiquidity`                             | number | –        | USD                                                                                                            |
| `minVaultScore`                            | number | –        | 0–100. Vaults with no computed score are excluded — an unscored vault cannot satisfy a floor.                  |
| `version`                                  | enum   | –        | `v1` \| `v2` \| `all` (default)                                                                                |
|                                            |        |          | Plus the shared parameters.                                                                                    |

**Response:** list envelope of vault objects.

`view=slim` returns identity, asset, APY, TVL and tags only — same vaults, same order, same filters, smaller payload:

```jsonc
{
  "vaultId": "plume:0x0b14…", "address": "0x0b14…", "name": "Mystic MEV Capital pUSD",
  "network": { "slug": "plume", "chainId": 98866 },
  "protocol": { "name": "morpho", "version": "v1.1" },
  "asset": { "address": "0xdddd…", "symbol": "pUSD", "assetGroup": "usd" },
  "apy": { /* … */ }, "tvl": { "native": "…", "usd": "…" }, "tags": ["stablecoin"]
}
```

It is a parameter rather than a second route so filtering and sorting cannot diverge between the two.

#### `GET /v1/yields/vaults/{network}/{vaultId}`

**Request**

| Field     | Type | Required | Description                 |
| --------- | ---- | -------- | --------------------------- |
| `network` | path | ✅        | Network slug, e.g. `plume`. |
| `vaultId` | path | ✅        | Vault contract address.     |

**Response** — one vault object, including the detail-only fields. `404` when it does not exist.

**The vault object**

```jsonc
{
  "vaultId": "plume:0x0b14…",          // {networkSlug}:{address} — globally unique
  "address": "0x0b14…",
  "name": "Mystic pUSD Core",
  "network":  { "name": "Plume", "slug": "plume", "chainId": 98866, "networkCaip": "eip155:98866" },
  "protocol": { "name": "morpho", "displayName": "Morpho", "product": "vault", "version": "v1.1" },
  "asset": {
    "address": "0xdddd…", "symbol": "pUSD", "decimals": 6,
    "priceUsd": "1.0002",              // null when unpriceable — never 1
    "assetGroup": "usd",
    "assetCaip": "eip155:98866/erc20:0xdddd…",
    "logoUri": "https://…",            // mirrored from Morpho's registry; null if they have none
    "intrinsicApy": null               // the asset's OWN yield (LST, tokenised T-bill); additive
  },
  "lpToken": { "address": "0x0b14…", "symbol": "MEVPUSD", "decimals": 18 },
  "apy": {
    "instantaneous": { "base": 0.0642, "reward": 0.0110, "total": 0.0752 },
    "1hour": { "base": 0.0636, "reward": 0.0140, "total": 0.0776 },
    "1day":  { "base": 0.0612, "reward": 0.0140, "total": 0.0752 },
    "7day":  { "base": 0.0591, "reward": 0.0140, "total": 0.0731 },
    "30day": { "base": 0.0575, "reward": 0.0138, "total": 0.0713 }
  },
  "tvl":       { "native": "12397600.0", "usd": "12400079.5" },
  "liquidity": { "native": "3120004.1",  "usd": "3120628.1" },   // native is in ASSET units
  "sharePrice": { "value": "1.041233", "asset": "pUSD" },
  "fees": {
    "performanceFee": 0.1,             // fraction, so 0.1 = 10%
    "managementFee": null,             // V2 only — V1 has no such fee
    "withdrawalFee": null,             // Morpho has none, in either version
    "depositFee": null
  },
  "capacity": { "maxCapacity": "50000000.0", "remainingCapacity": "37602400.0" },
  "curator":  { "name": "Mystic", "logoUri": "https://…" },
  "curators": [{ "name": "Mystic" }, { "name": "Cicada" }],       // incl. co-curators
  "rewards":  [{ "asset": null, "apy": 0.014, "source": "merkl" }],
  "holderCount": 137,                  // at the last daily snapshot; null where we run no indexer
  "score": { "vaultScore": 78 },       // headline only in lists; breakdown on detail / ?expand=score
  "tags": ["stablecoin", "blue-chip", "incentivised"],
  "collaterals": [{ "symbol": "pETH", "address": "0x…", "lltv": 0.86, "oracle": "0x…" }],
  "flags": [], "warnings": [],
  "creationData": { "deploymentTimestamp": 1719878400 },
  "lastUpdateTimestamp": 1753747200,

  // ---- detail responses only ----
  "allocations": [
    { "kind": "market", "share": 0.62, "amount": { "native": "…", "usd": "…" },
      "market": { /* the COMPLETE market object — see below */ },
      "position": { "supplyShares": "…", "borrowShares": null, "collateral": null }, "vault": null },
    { "kind": "vault",  "share": 0.28, "amount": { /* … */ },
      "vault": { "vaultId": "plume:0x…", "address": "0x…", "name": "…", "version": "v1.1", "asset": "pUSD" } },
    { "kind": "idle",   "share": 0.10, "amount": { /* … */ } }
  ],
  "governance": {
    "owner": "0x…", "curator": "0x…", "guardian": null,
    "feeRecipient": "0x…", "skimRecipient": null, "timelockSeconds": 86400
  },
  "protocolSpecific": { "feeWrapper": "0x…", "vaultV2AdapterAddress": null }
}
```

`instantaneous` is the spot rate,a null APY window is meaningful. It means no data covers that period, a two-day-old vault genuinely has no 30-day APY. The spot rate is never copied in.

Three allocation kinds exists, the first is the idle for idle liquidity, the second is market for market allocation and the third is vault for vault allocation. The vault allocation is only available for v2 vaults.

#### `GET /v1/yields/vaults/{network}/{vaultId}/apy`

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "apy": { /* the full interval block */ },
  "rewards": [{ "asset": null, "apy": 0.014, "source": "merkl" }],
  "lastUpdateTimestamp": 1753747200
}
```

#### `GET /v1/yields/vaults/{network}/{vaultId}/tvl`

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "tvl": { "native": "12397600.0", "usd": "12400079.5" },
  "liquidity": { "native": "3120004.1", "usd": "3120628.1" },
  "lastUpdateTimestamp": 1753747200
}
```

#### `GET /v1/yields/vaults/{network}/{vaultId}/allocations`

Largest share first. The same array the detail response carries inline.

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "asset": { "symbol": "pUSD", "address": "0xdddd…", "priceUsd": "1.0002", "assetGroup": "usd" },
  "data": [ /* allocation entries — see the vault object */ ],
  "lastUpdateTimestamp": 1753747200
}
```

#### `GET /v1/yields/vaults/{network}/{vaultId}/campaigns`

**Response**

```jsonc
{
  "target": "plume:0x0b14…",
  "source": "merkl",
  "available": true,               // false when Merkl returned nothing for the chain at all
  "data": [{
    "id": "0xabc…", "name": "pUSD Boost", "type": "ERC20", "status": "LIVE",
    "apy": 0.014,                  // Merkl publishes a percentage; normalised to a fraction here
    "dailyRewardsUsd": "412.55",
    "tvlUsd": "12400079.5",
    "rewardTokens": [{ "symbol": "PLUME", "address": "0xea23…", "endTimestamp": 1756339200 }],
    "startTimestamp": 1753747200,
    "endTimestamp": 1756339200
  }],
  "pagination": { /* … */ },
  "errors": { /* … */ }
}
```

#### `GET /v1/yields/vaults/{network}/{vaultId}/holders`

Requires an indexer. See coverage.

**Request** — `page`, `perPage` (default 50, max 500).

**Response**

```jsonc
{
  "data": [{ "address": "0x…", "shares": "412000.0", "assets": "429142.7" }],
  "pagination": { /* … */ },
  "totalCount": 137,
  "unavailableReason": null
}
```

On an unindexed network the list is empty, `totalCount` is `null`, the network is in `errors.unsupportedNetworks`, and `unavailableReason` says why — rather than an empty array that reads as "this vault has no holders".

***

### Borrows

| Route                                              | Credits | Description                          |
| -------------------------------------------------- | ------- | ------------------------------------ |
| `/borrow/markets`                                  | 1       | List markets                         |
| `/borrow/markets/{network}/{marketId}`             | 1       | One market                           |
| `/borrow/markets/{network}/{marketId}/allocations` | 1       | Which vaults supply it, and how much |

#### `GET /v1/yields/borrow/markets`

**Request**

| Field                                                    | Type    | Required | Description                                                                            |
| -------------------------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------- |
| `sortBy`                                                 | enum    | –        | `tvl` (default), `supplyApy`, `borrowApy`, `utilization`, `liquidity`, `lltv`, `score` |
| `expand`                                                 | string  | –        | `supplyingVaults`, `score`. Unrecognised values are a `400`.                           |
| `allowedCollateralAssets` / `disallowedCollateralAssets` | string  | –        | Symbol or address                                                                      |
| `allowedBorrowAssets` / `disallowedBorrowAssets`         | string  | –        | The loan asset, by symbol or address                                                   |
| `allowedProtocols` / `disallowedProtocols`               | string  | –        | See `/protocols`                                                                       |
| `tags`                                                   | string  | –        | See `/tags`                                                                            |
| `minTvl` / `maxTvl`                                      | number  | –        | Total supplied, USD                                                                    |
| `minLltv` / `maxLltv`                                    | number  | –        | **Fractions.** `0.86` is 86%.                                                          |
| `minUtilization` / `maxUtilization`                      | number  | –        | Fractions                                                                              |
| `minBorrowApy` / `maxBorrowApy`                          | number  | –        | Fractions                                                                              |
| `minMarketScore`                                         | number  | –        | 0–100                                                                                  |
| `hasLiquidity`                                           | boolean | –        | Only markets with borrowable liquidity                                                 |
| `includeBadDebt`                                         | boolean | –        | Default `false`                                                                        |
| `includeUnverified`                                      | boolean | –        | Default `false`                                                                        |
|                                                          |         |          | Plus the shared parameters.                                                            |

**Response:** list envelope of market objects.

#### `GET /v1/yields/borrow/markets/{network}/{marketId}`

**Request**

| Field      | Type | Required | Description                                         |
| ---------- | ---- | -------- | --------------------------------------------------- |
| `network`  | path | ✅        | Network slug.                                       |
| `marketId` | path | ✅        | The 32-byte market id (without the network prefix). |

**The market object**

```jsonc
{
  "marketId": "plume:0x7a96549c…",     // {networkSlug}:{marketId}
  "marketKey": "0x7a96549c…",          // the raw 32-byte id
  "network":  { "name": "Plume", "slug": "plume", "chainId": 98866, "networkCaip": "eip155:98866" },
  "protocol": { "name": "morpho", "displayName": "Morpho", "product": "lending-market", "version": "v1.1" },
  "loanAsset":       { "address": "0xdddd…", "symbol": "pUSD", "priceUsd": "1.0002", "assetGroup": "usd" },
  "collateralAsset": { "address": "0x593c…", "symbol": "nALPHA", "priceUsd": "1.1059", "assetGroup": "rwa" },
  "lltv": 0.86,
  "liquidationThreshold": 0.86,
  "liquidationPenalty": 0.0438,
  "apy": {
    "supply": { "base": 0.0373, "reward": 0, "total": 0.0373 },
    "borrow": { "base": 0.0426, "reward": 0, "total": 0.0426 }
  },
  "supply": { "assets": "4261152.37", "shares": "4048331.17", "usd": "4262004.6" },
  "borrow": { "assets": "3747270.67", "shares": "3535370.85", "usd": "3748020.1" },
  "liquidity":              { "native": "513881.7", "usd": "513984.5" },
  "reallocatableLiquidity": { "native": "109354.5", "usd": "109376.3" },
  "utilization": 0.8794,
  "capacity": { "supplyCap": "10000000.0", "totalSupplyCaps": "11000000.0" },
  "fee": 0,
  "performanceFee": 0.1,               // a DIFFERENT number from `fee`
  "isIdle": false,                     // Morpho's placeholder market, flagged not inferred
  "oracle": { "address": "0x7824…", "baseFeed": null },
  "irm":    { "address": "0x7420…" },
  "score": { "marketScore": 62 },      // identical on list and detail by construction
  "supplyingVaults": [{
    "vaultId": "plume:0xc0df…", "address": "0xc0df…", "name": "Re7 RWA Yield", "symbol": "Re7pUSD",
    "curators": [{ "name": "Re7", "address": "0x…" }],
    "supplyShare": 0.9999,             // fraction of the market's total supply
    "supplied": { "native": "4048324.07", "usd": "4049133.7" },
    "vaultAsset": "pUSD"
    // ?expand=supplyingVaults adds: asset, curator, marketLabel, isActive, feeWrapper,
    // accessType, lastUpdateTimestamp, borrowShare, suppliedAssets, borrowedAssets,
    // cap, allocated, allocatorFee
  }],
  "vaultCaps": [{
    "vaultAddress": "0xc0df…", "vaultId": "plume:0xc0df…", "marketKey": "0x7a96549c…",
    "supplyCap": "10000000.0", "allocationLiquidity": "1093545.29", "allocationFee": 0
  }],
  "tags": [], "flags": [],
  "lastAccrualTimestamp": 1785516494,
  "lastUpdateTimestamp": 1785516534
}
```

**`liquidity` vs `reallocatableLiquidity`.** The first is total reachable liquidity: the market's own free liquidity plus whatever a public allocator can move in. The second is that reallocatable component alone. They behave differently, free liquidity is available now, reallocatable liquidity depends on an allocator acting, and a borrower sizing a draw needs to know which is which.

**`supplyingVaults` is tiered.** Lists carry the identity fields (everything `/morphoCache/lite` published, plus ids); the detail endpoint and `?expand=supplyingVaults` carry every field. Expanded fields are **omitted** rather than nulled, so `'cap' in vault` distinguishes "not asked for" from "no cap set".

**`vaultCaps` is not `supplyingVaults`.** It is what a public allocator *could* move, which is the number that decides whether a large borrow can be filled.

#### `GET /v1/yields/borrow/markets/{network}/{marketId}/allocations`

```jsonc
{
  "marketId": "plume:0x7a96549c…",
  "data": [ /* supplyingVaults, fully expanded */ ],
  "lastUpdateTimestamp": 1785516534
}
```

***

### Portfolio

Wallet-scoped reads. Every response carries `errors.unsupportedNetworks` naming any chain we could not see, for a balance figure that matters more than anywhere else in the API.

| Route                                                      | Credits | Description                             |
| ---------------------------------------------------------- | ------- | --------------------------------------- |
| `/portfolio/summary/{address}`                             | 3       | Everything below, in one call           |
| `/portfolio/positions/{address}`                           | 2       | Vault positions across networks         |
| `/portfolio/positions/{address}/{network}/{vaultId}`       | 2       | One position                            |
| `/portfolio/borrow-positions/{address}`                    | 2       | Collateral, debt, LTV, health factor    |
| `/portfolio/total-returns/{address}/{network}/{vaultId}`   | 2       | Lifetime realised + unrealised return   |
| `/portfolio/partial-returns/{address}/{network}/{vaultId}` | 2       | Return between two timestamps           |
| `/portfolio/historical-positions/{address}`                | 2       | Every vault ever held, including exited |
| `/portfolio/historical-balances/{address}/{timestamp}`     | 2       | Balances reconstructed at a past date   |
| `/portfolio/idle-assets/{address}`                         | 2       | Holdings not deployed anywhere we index |
| `/portfolio/best-vault/{address}`                          | 3       | Best vault per idle asset               |
| `/portfolio/best-deposit-options/{address}`                | 3       | Top N vaults per idle asset             |

**Shared portfolio parameters**

| Field                  | Type    | Required | Description                                                                                                  |
| ---------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `address`              | path    | ✅        | Wallet address.                                                                                              |
| `page`, `perPage`      | integer | –        | Default 1 / 50, max 500.                                                                                     |
| `allowedNetworks`      | csv     | –        | Slugs or chain ids.                                                                                          |
| `allowedAssets`        | csv     | –        | Symbols or addresses.                                                                                        |
| `minUsdValueThreshold` | number  | –        | Default `0.01`. Suppresses dust — a wallet that ever touched a vault keeps a rounding-dust position forever. |

#### `GET /v1/yields/portfolio/summary/{address}`

Replaces roughly five separate requests, and is priced accordingly.

```jsonc
{
  "address": "0x…",
  "totals": {
    "suppliedUsd": "124000.5", "collateralUsd": "50000",
    "debtUsd": "20000", "netWorthUsd": "154000.5"
  },
  "weightedApy": 0.0641,                   // TVL-weighted, not a mean of rates
  "projectedUsdAnnualEarnings": "7948.4",
  "positionCount": 4,
  "borrowPositionCount": 1,
  "lowestHealthFactor": 2.15,              // the riskiest position governs
  "networks": ["plume", "flare"],
  "errors": { /* … */ }
}
```

An average health factor would hide one position about to be liquidated behind several safe ones, so the lowest is reported instead.

#### `GET /v1/yields/portfolio/positions/{address}`

**Response** — list envelope of positions.

```jsonc
{ "data": [{
  "vaultId": "plume:0x0b14…", "address": "0x0b14…", "name": "Mystic MEV Capital pUSD",
  "network":  { "slug": "plume", "chainId": 98866, "name": "Plume", "networkCaip": "eip155:98866" },
  "protocol": { "name": "morpho", "displayName": "Morpho", "product": "vault", "version": "v1.1" },
  "asset": { "address": "0xdddd…", "symbol": "pUSD", "decimals": 6, "priceUsd": "1.0002", "assetGroup": "usd" },
  "balance": { "shares": "119843.22", "native": "124776.5", "usd": "124801.4" },
  "apy": { "base": 0.0612, "reward": 0.0140, "total": 0.0752 },
  "projectedUsdAnnualEarnings": "9384.9"
}] }
```

#### `GET /v1/yields/portfolio/positions/{address}/{network}/{vaultId}`

One position object as above. `404` when the wallet holds nothing in that vault.

#### `GET /v1/yields/portfolio/borrow-positions/{address}`

The indexer models collateral and debt as separate rows; they are re-joined here so a position is one object, not two halves.

```jsonc
{ "data": [{
  "marketId": "plume:0x7a96549c…",
  "network": { "slug": "plume", "chainId": 98866 },
  "protocol": { "name": "morpho", "displayName": "Morpho", "product": "lending-market" },
  "collateralAsset": { "symbol": "nALPHA", "address": "0x593c…", "decimals": 6, "priceUsd": "1.1059" },
  "debtAsset":       { "symbol": "pUSD",   "address": "0xdddd…", "decimals": 6, "priceUsd": "1.0002" },
  "collateral": { "native": "10000.0", "usd": "11059.9" },
  "debt":       { "native": "6000.0",  "usd": "6001.2" },
  "lltv": 0.86,
  "ltv": 0.5426,
  "healthFactor": 1.585                    // null when there is no debt
}] }
```

`healthFactor` is collateral × liquidation threshold ÷ debt. It is **`null` with no debt** — a position that cannot be liquidated has no health factor, and returning `Infinity` would force every caller to special-case it.

#### `GET /v1/yields/portfolio/total-returns/{address}/{network}/{vaultId}`

Current value plus everything withdrawn minus everything deposited, denominated in the **vault asset** — a USD figure would conflate yield with the asset's own price movement.

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "asset": { "symbol": "pUSD", "address": "0xdddd…", "priceUsd": "1.0002" },
  "deposited":    { "native": "120000.0" },
  "withdrawn":    { "native": "10000.0" },
  "currentValue": { "native": "124776.5" },
  "returnsNative": "14776.5",
  "returnsUsd": "14779.4",
  "basedOnEvents": 42,
  "isComplete": true                        // false once the 500-event cap truncates the history
}
```

#### `GET /v1/yields/portfolio/partial-returns/{address}/{network}/{vaultId}`

Period performance rather than lifetime. Contributions inside the window are subtracted, so a mid-window deposit does not read as a gain.

**Request** — `fromTimestamp` **and** `toTimestamp` are both **required** (a period return needs a period); `toTimestamp` must be after `fromTimestamp`. Either omitted, or reversed, is a `400`.

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "asset": { "symbol": "pUSD" },
  "fromTimestamp": 1735689600, "toTimestamp": 1767225600,
  "openingContributed":  { "native": "100000.0" },
  "depositedInWindow":   { "native": "20000.0" },
  "withdrawnInWindow":   { "native": "10000.0" },
  "closingValue":        { "native": "124776.5" },
  "returnsNative": "14776.5", "returnsUsd": "14779.4",
  "eventsInWindow": 12,
  "closingValueIsCurrent": true,            // see below
  "isComplete": true
}
```

`closingValueIsCurrent` is stated rather than implied: a `toTimestamp` in the past cannot be honoured exactly, because we hold no historical share price for every vault. The closing value is the **current** balance.

#### `GET /v1/yields/portfolio/historical-positions/{address}`

Every vault the wallet has ever held, including fully-exited ones, with when each opened and last saw activity. `positions` only shows current holdings, so a wallet that exited last month vanishes from it entirely.

**Request** — the shared portfolio parameters plus `fromTimestamp`, `toTimestamp`, `eventType`, and `includeClosed` (default `true`).

#### `GET /v1/yields/portfolio/historical-balances/{address}/{timestamp}`

Replayed from the transfer stream: Σdeposits − Σwithdrawals up to that instant.

**Request:** `timestamp` is a path parameter in unix seconds; non-numeric or non-positive is a `400`.

```jsonc
{
  "address": "0x…",
  "asOf": 1735689600,
  "basis": "net-contributed",
  "caveats": [
    "Reconstructed from transfer events, so yield accrued between events is not included.",
    "Valued at the current asset price, not the price on the requested date."
  ],
  "data": [{
    "vaultId": "plume:0x0b14…",
    "network": { "slug": "plume", "chainId": 98866 },
    "asset": { "symbol": "pUSD" },
    "netContributed": { "native": "110000.0" },
    "valueUsdAtCurrentPrice": "110022.0",
    "eventsReplayed": 31
  }],
  "pagination": { /* … */ }, "errors": { /* … */ }
}
```

It reports **net contributed, not position value** — yield accrued between events is not recoverable from transfers alone. Both limits are on the response rather than in a footnote.

#### `GET /v1/yields/portfolio/idle-assets/{address}`

Wallet balances not deployed into anything we index.

**Request**

| Field                                | Type    | Required | Description                                      |
| ------------------------------------ | ------- | -------- | ------------------------------------------------ |
| `address`                            | path    | ✅        | Wallet address.                                  |
| `page`, `perPage`                    | integer | –        | Default 1 / 50, max 200.                         |
| `allowedNetworks`                    | csv     | –        | Slugs or chain ids.                              |
| `allowedAssets` / `disallowedAssets` | csv     | –        | Symbols or addresses.                            |
| `minUsdValueThreshold`               | number  | –        | Default `1` — below that, gas exceeds the yield. |

```jsonc
{ "data": [{
  "address": "0xdddd…", "symbol": "pUSD", "decimals": 6,
  "priceUsd": "1.0002", "assetGroup": "usd",
  "assetCaip": "eip155:98866/erc20:0xdddd…",
  "network": { "slug": "plume", "chainId": 98866 },
  "balanceNative": "5000.0", "balanceUsd": "5001.0"
}] }
```

**What the scope actually is.** Balances are read against a **curated asset universe** — every asset our vaults and markets use, plus the chain's native token and a short extras list. It is not every ERC-20 in the wallet.

That is a deliberate trade: an open scan needs a token indexer we do not run on Ethereum or Base, and it surfaces airdropped spam with fabricated prices, which in a "here is what you could be earning" endpoint is actively harmful. The consequence is that a holding outside the universe will not appear. "Idle" is also relative to what we index — a token deployed in a protocol we do not cover reads as idle here.

#### `GET /v1/yields/portfolio/best-vault/{address}`

Highest-yielding vault for each idle asset, with projected annual earnings on that balance. Restricted to vaults denominated in the **same asset** — a recommendation that silently requires a swap would be misleading.

**Request:** the idle-asset parameters plus:

| Field                | Type    | Required | Description                                                                   |
| -------------------- | ------- | -------- | ----------------------------------------------------------------------------- |
| `apyInterval`        | enum    | –        | Window to rank on. Default `1day`.                                            |
| `minTvl`             | number  | –        | Only consider vaults above this TVL, USD.                                     |
| `maxVaultsPerAsset`  | integer | –        | Default `3`, max `10`. Used by `best-deposit-options`.                        |
| `alwaysReturnAssets` | boolean | –        | Default `false`. Keep assets with no matching vault instead of dropping them. |

```jsonc
{
  "requestedAddress": "0x…",
  "data": [{
    "asset": { "symbol": "pUSD", "balanceNative": "5000.0", "balanceUsd": "5001.0", "network": { "slug": "plume" } },
    "vault": {
      "vaultId": "plume:0x0b14…", "name": "Mystic MEV Capital pUSD",
      "network": { /* … */ }, "protocol": { /* … */ },
      "tvl": { "native": "…", "usd": "…" },
      "apy": { "base": 0.0612, "reward": 0.0140, "total": 0.0752 },
      "tags": ["stablecoin"], "curator": { "name": "MEV" },
      "projectedUsdAnnualEarnings": "376.1"
    }
  }],
  "errors": { /* … */ }
}
```

#### `GET /v1/yields/portfolio/best-deposit-options/{address}`

Same request and shape, but each entry carries a `vaults` array of up to `maxVaultsPerAsset` options instead of a single `vault`.

`projectedUsdAnnualEarnings` is the point: an APY alone does not tell a user whether moving $40 is worth the gas.

***

### Transactions

A wallet's on-chain activity against everything we index — the feed a portfolio UI renders as a history tab. The separate `/liquidations` API is market-scoped: it answers "what was liquidated in this market", not "what happened to me".

| Route                                             | Credits | Description                     |
| ------------------------------------------------- | ------- | ------------------------------- |
| `/portfolio/events/{address}`                     | 3       | Every event against this wallet |
| `/portfolio/events/{address}/{network}/{vaultId}` | 2       | Events for one vault            |

#### `GET /v1/yields/portfolio/events/{address}`

Deposits, withdrawals, borrows, repayments and **liquidations**, newest first.

**Request**

| Field                           | Type    | Required | Description                                             |
| ------------------------------- | ------- | -------- | ------------------------------------------------------- |
| `address`                       | path    | ✅        | Wallet address.                                         |
| `eventType`                     | string  | –        | `deposit`, `withdraw`, `borrow`, `repay`, `liquidation` |
| `fromTimestamp` / `toTimestamp` | integer | –        | Unix seconds.                                           |
| `includeClosed`                 | boolean | –        | Default `true`.                                         |
|                                 |         |          | Plus the shared portfolio parameters.                   |

**Response**

```jsonc
{ "data": [{
  "timestamp": 1753747200,
  "blockNumber": "18402113",
  "eventType": "deposit",
  "network": { "slug": "plume", "chainId": 98866, "name": "Plume", "networkCaip": "eip155:98866" },
  "vaultId": "plume:0x0b14…",              // null on market events
  "marketId": null,                        // set on borrow/repay/liquidation
  "asset": { "address": "0xdddd…", "symbol": "pUSD", "decimals": 6, "priceUsd": "1.0002" },
  "assetAmount": { "native": "20000.0", "usd": "20004.0" },
  "lpTokenAmount": "19207.4",              // shares moved; null on market events
  "transactionHash": "0x…",
  "logIndex": null                         // set on market events
}] }
```

A liquidation carries an extra block:

```jsonc
{
  "eventType": "liquidation",
  "marketId": "plume:0x7a96549c…",
  "assetAmount": { "native": "6000.0", "usd": "6001.2" },
  "liquidation": {
    "liquidator": "0x…",
    "seizedAsset": { "symbol": "nALPHA", "address": "0x593c…", "decimals": 6 },
    "seizedAmount": "5660.4"
  }
}
```

#### `GET /v1/yields/portfolio/events/{address}/{network}/{vaultId}`

The same feed, scoped to one vault. Same parameters, same event shape.

***

### Historical

Every route takes an explicit window and an optional granularity. This replaces the legacy `timeRange=1M` enum, which cannot express "since I deposited" — the range a portfolio UI actually needs.

| Route                                          | Credits | Description                                               |
| ---------------------------------------------- | ------- | --------------------------------------------------------- |
| `/historical/{network}/{vaultId}`              | 2       | TVL, APY, liquidity and implied asset price in one series |
| `/historical/{network}/{vaultId}/apy`          | 2       | APY only                                                  |
| `/historical/{network}/{vaultId}/tvl`          | 2       | TVL only                                                  |
| `/historical/{network}/{vaultId}/share-price`  | 3       | Assets per share over time                                |
| `/historical/{network}/{vaultId}/daily-flows`  | 3       | Net deposits and withdrawals per day                      |
| `/historical/asset-prices/{network}/{address}` | 3       | Asset price over time                                     |
| `/historical/borrow/{network}/{marketId}`      | 2       | Market supply, borrow, rates, utilization                 |

**Shared parameters**

| Field           | Type    | Required | Description                                            |
| --------------- | ------- | -------- | ------------------------------------------------------ |
| `fromTimestamp` | integer | –        | Start of the window, unix seconds, inclusive.          |
| `toTimestamp`   | integer | –        | End of the window, unix seconds, inclusive.            |
| `granularity`   | enum    | –        | `1hour` \| `1day` \| `1week`. Omit for the raw series. |
| `page`          | integer | –        | Default `1`.                                           |
| `perPage`       | integer | –        | Default `500`, max `1000`.                             |

Buckets are aligned to the epoch and keep their **closing** value. These are point-in-time readings, so averaging within a bucket would invent a number that never existed at any instant.

#### `GET /v1/yields/historical/{network}/{vaultId}`

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "data": [{
    "timestamp": 1753747200, "date": "2025-07-29",
    "tvl": { "native": "12397600.0", "usd": "12400079.5" },
    "apy": 0.0612,
    "liquidity": "3120004.1",
    "assetPriceUsd": "1.0002"
  }],
  "pagination": { /* … */ }, "errors": { /* … */ }
}
```

`404` when we hold no history for that vault.

#### `GET /v1/yields/historical/{network}/{vaultId}/apy`

```jsonc
{ "data": [{ "timestamp": 1753747200, "apy": 0.0612 }], "pagination": { /* … */ } }
```

#### `GET /v1/yields/historical/{network}/{vaultId}/tvl`

```jsonc
{ "data": [{ "timestamp": 1753747200, "tvl": { "native": "12397600.0", "usd": "12400079.5" } }] }
```

#### `GET /v1/yields/historical/{network}/{vaultId}/share-price`

Requires indexed share supply.

```jsonc
{
  "data": [{ "timestamp": 1753747200, "sharePrice": "1.041233" }],
  "pagination": { /* … */ },
  "unavailableReason": null
}
```

On Ethereum and Base it returns an **empty series with an explicit `unavailableReason`** and the network in `errors.unsupportedNetworks`, rather than an empty array that reads like "this vault has no history".

#### `GET /v1/yields/historical/{network}/{vaultId}/daily-flows`

Net flow per day, counted from actual transfers.

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "data": [{
    "timestamp": 1753747200, "date": "2025-07-29",
    "deposits": "42000.0", "withdrawals": "18000.0",
    "netFlow": "24000.0",                 // signed — negative days are outflows
    "depositCount": 7, "withdrawalCount": 3
  }],
  "pagination": { /* … */ },
  "unavailableReason": null
}
```

Counted from transfers rather than differenced from TVL: a TVL delta conflates flows with yield accrual and price movement, so a vault that took no deposits but whose asset appreciated would show a fabricated inflow. Requires an indexer.

#### `GET /v1/yields/historical/asset-prices/{network}/{address}`

```jsonc
{
  "asset": { "address": "0xdddd…", "network": "plume" },
  "source": "recorded",                     // or "implied"
  "sourceVault": null,                      // set when source is "implied"
  "data": [{ "timestamp": 1753747200, "date": "2025-07-29", "priceUsd": "1.0002" }],
  "pagination": { /* … */ }
}
```

Two sources, named so you can tell them apart — they have different accuracy:

* **`recorded`** — read from the daily price snapshots. Authoritative, and covers every asset we index including collateral assets no vault is denominated in.
* **`implied`** — derived from a vault's own history, where each point's USD and native totals give the price the pipeline observed. Used for the period before daily recording began, with `sourceVault` naming the vault it came from.

`404` when the asset is neither recorded nor held by any indexed vault.

#### `GET /v1/yields/historical/borrow/{network}/{marketId}`

```jsonc
{
  "marketId": "plume:0x7a96549c…",
  "data": [{
    "timestamp": 1753747200, "date": "2025-07-29",
    "supply": { "native": "4261152.37", "usd": "4262004.6" },
    "borrow": { "native": "3747270.67", "usd": "3748020.1" },
    "liquidity": "513881.7",
    "apy": { "supply": 0.0373, "borrow": 0.0426 },
    "utilization": 0.8794
  }],
  "pagination": { /* … */ }
}
```

***

### Realtime

Uncached on-chain reads, for when a cache is not good enough — sizing a liquidation, or reconciling against chain state.

| Route                                                  | Credits | Description                                               |
| ------------------------------------------------------ | ------- | --------------------------------------------------------- |
| `/realtime/{network}/{vaultId}`                        | 3       | Total assets, total supply and share price from ONE block |
| `/realtime/{network}/{vaultId}/share-price`            | 3       | Share price alone                                         |
| `/realtime/{network}/{vaultId}/total-assets`           | 3       | Total assets alone                                        |
| `/realtime/{network}/{vaultId}/total-supply`           | 3       | Total share supply alone                                  |
| `/realtime/{network}/{vaultId}/underlying-asset-price` | 3       | Asset identity live; price from the cache                 |

#### `GET /v1/yields/realtime/{network}/{vaultId}`

**Request:** `network` and `vaultId` path parameters. No query parameters.

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "network": { "name": "Plume", "slug": "plume", "chainId": 98866, "networkCaip": "eip155:98866" },
  "readAt": 1785516534,
  "asset": { "address": "0xdddd…", "symbol": "pUSD", "decimals": 6, "priceUsd": "1.0002" },
  "totalAssets": { "native": "12397600.0", "usd": "12400079.5" },
  "totalSupply": "11907214.2",
  "sharePrice": "1.041233"                  // null for a vault with no shares outstanding
}
```

**Prefer this route** if you need more than one value. It is one RPC round-trip instead of several and more importantly the values come from the **same block**. Four separate calls can straddle a block and produce a share price that never existed. The combined route costs the same as a single sub-resource, so taking the consistent path is never penalised.

`sharePrice` is `null`, never `1.0`, for a vault with no shares outstanding: 1.0 is a plausible-looking reading and would be indistinguishable from a real one.

#### The single-value routes

Each reads the same block and returns one slice of the snapshot above.

| Route                                                      | Response                                                                              |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `GET /v1/yields/realtime/{network}/{vaultId}/share-price`  | `{ vaultId, readAt, sharePrice, asset }` — `asset` is the symbol here, not the object |
| `GET /v1/yields/realtime/{network}/{vaultId}/total-assets` | `{ vaultId, readAt, totalAssets: { native, usd } }`                                   |
| `GET /v1/yields/realtime/{network}/{vaultId}/total-supply` | `{ vaultId, readAt, totalSupply }`                                                    |

They cost the same as the combined route, so there is no saving in asking for one value at a time — only the risk of straddling a block.

#### `GET /v1/yields/realtime/{network}/{vaultId}/underlying-asset-price`

```jsonc
{
  "vaultId": "plume:0x0b14…",
  "readAt": 1785516534,
  "asset": { "address": "0xdddd…", "symbol": "pUSD", "decimals": 6, "priceUsd": "1.0002" },
  "priceSource": "cache"
}
```

Only the asset **identity** is read on-chain. The price comes from the same pricing layer the cached endpoints use, and `priceSource` says so — a caller reconciling against chain data needs to know which half is live.

#### Liquidations

| Endpoint                                                                       | Description                                                                                 |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `GET /liquidations/overview-data?chainId={chainId}&marketId={marketId}`        | Aggregate summary across all matching positions (counts + USD totals).                      |
| `GET /liquidations/positions?chainId={chainId}&marketId={marketId}`            | Paginated list of open borrow positions and their risk of liquidation (LTVs, HFs and more). |
| `GET /liquidations/liquidated-positions?chainId={chainId}&marketId={marketId}` | Paginated history of positions that have already been liquidated (most recent first).       |

### Supported protocols, and chains

#### Protocols

| Protocol | Product                  | Versions | Vaults | Borrow markets | Events | Share-price history | Holders | Positions |
| -------- | ------------------------ | -------- | ------ | -------------- | ------ | ------------------- | ------- | --------- |
| Morpho   | Vaults + lending markets | V1.1, V2 | ✅      | ✅              | ✅      | ✅                   | ✅       | ✅         |

#### Chains

Every endpoint takes a `chainId` query param. Omit it and it defaults to **Plume (98866)**. Unsupported chains return an empty result, not an error.\
\
This is a list of Chains and their respective chainId below.

| Chain    | ChainId |
| -------- | ------- |
| Plume    | 98866   |
| Flare    | 14      |
| Citrea   | 4114    |
| Ethereum | 1       |
| Base     | 8453    |

### Errors

| Status | Meaning                                                                                                         | What to do                                                                                                                                                  |
| ------ | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Unknown query parameter, wrong type, or a missing required one (e.g. `partial-returns` without both timestamps) | Fix the request. The message names the offending property.                                                                                                  |
| `401`  | Missing, invalid, disabled or expired API key                                                                   | Check the `x-api-key` header.                                                                                                                               |
| `402`  | Monthly credit allowance exhausted, on a key with a hard cap                                                    | Upgrade the plan or ask support to raise the ceiling. Only keys explicitly opted into a spend ceiling can hit this — the default is that overage is billed. |
| `404`  | Resource does not exist, or no history for it                                                                   | Do not retry. Never returned as `200 null`.                                                                                                                 |
| `429`  | Over your plan's requests/second                                                                                | Back off for `Retry-After` seconds.                                                                                                                         |
| `5xx`  | Our problem                                                                                                     | Retry with backoff; contact support if it persists.                                                                                                         |

#### What is not an error

Three things that look like failures and are not:

**Unresolvable filter values.** `?allowedNetworks=plume,atlantis` returns `200` with every Plume result and `errors.unsupportedNetworks: ["atlantis"]`. Losing nine good networks to one typo would be worse than the warning.

**Null values.** `priceUsd: null`, `apy["30day"]: null`, `healthFactor: null` and `sharePrice: null` are all deliberate. They mean "we do not know" or "not applicable".


# Mystic Morpho GraphQL API

Read real-time Morpho data on the chains Mystic is on, including vaults and markets data, APYs, TVL, liquidity, rewards and much more.

### Introduction

The Mystic Morpho API lets you read all kinds of Morpho data in real-time on the chains Morpho is on. Track your positions, liquidate, manage risk or build your own Morpho-powered app using Mystic's Morpho API. Note that to use it, you will need your own API Key. Please contact us to get an API Key and get started -> [**Contact us**](mailto:joao.moreira@mysticfinance.xyz)**.**

### Base URL

{% tabs %}
{% tab title="Production" %}

```
https://api.mysticfinance.xyz/graphql
```

{% endtab %}

{% tab title="Staging" %}

```
https://staging-api.mysticfinance.xyz/graphql
```

{% endtab %}
{% endtabs %}

### Authentication

To call the API, you need to an API Key. API Key usage is billed monthly and the Key will expire if payment fails. An expired key returns `401`.&#x20;

#### Making a Request

GraphQL is **`POST` +** a JSON body, you must send `Content-Type: application/json` and a body containing `query` (and optionally `variables`).&#x20;

```
curl "https://api.mysticfinance.xyz/graphql" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "query": "query Markets($ids: [ChainId!]) { morphoMarkets(where: { chainId_in: $ids }, limit: 2) { items { marketId name supplyApy { total } } } }",
    "variables": { "ids": [98866] }
  }'
```

#### What you can Query

| Query                                   | Returns                                                                                     |
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
| `morphoMarkets(where, limit, skip)`     | Returns Data for Markets                                                                    |
| `morphoVaults(where, limit, skip)`      | Returns Data for Vaults                                                                     |
| `marketHistory(marketId!, chainId)`     | Daily history for one market                                                                |
| `vaultHistory(vaultAddress!, chainId)`  | Daily history for one vault                                                                 |
| `openPositions(chainId, …, page)`       | Paginated list of open borrow positions and their risk of liquidation (LTVs, HFs and more). |
| `liquidatedPositions(chainId, …, page)` | Past liquidations, newest first (paginated).                                                |

#### Example Queries

**Markets**

```graphql
query Markets {
  morphoMarkets(where: { chainId_in: [98866] }, limit: 50, skip: 0) {
    pageInfo { countTotal hasNextPage }
    items {
      marketId
      name
      isIdle
      lltv { formatted }
      utilization
      liquidationPenalty
      collateralAsset { symbol address priceUsd decimals }
      loanAsset { symbol address priceUsd decimals }
      totalSupplied { formatted usd }
      totalBorrowed { formatted usd }
      liquidityInMarket { formatted usd }
      supplyApy { base rewards { asset { symbol } apr } total }
      borrowApy { base total }
      chain { id name }
    }
  }
}
```

**Vaults**

```graphql
query Vaults {
  morphoVaults(where: { chainId_in: [98866] }, limit: 50, skip: 0) {
    pageInfo { countTotal hasNextPage }
    items {
      vaultAddress
      name
      decimals
      asset { symbol address priceUsd }
      metadata { curators { name } }
      totalSupplied { formatted usd }
      totalLiquidity { formatted usd }
      supplyApy { base rewards { asset { symbol } apr } total fee }
      supplyApy7d { total }
      supplyApy30d { total }
      performanceFee
      curatorAddress
      marketAllocations {
        market { marketId name }
        supplyCap { usd }
      }
    }
  }
}
```

**Market and Vaults History**

```graphql
query History {
  marketHistory(marketId: "0x…", chainId: 98866) {
    daily {
      bucketTimestamp
      totalSupplied { usd }
      totalBorrowed { usd }
      supplyApy30d { total }
      borrowApy30d { total }
    }
  }
  vaultHistory(vaultAddress: "0x…", chainId: 98866) {
    daily {
      bucketTimestamp
      totalSupplied { usd }
      supplyApy30d { total }
    }
  }
}
```

**Open Positions**

```graphql
query OpenPositions {
  openPositions(
    chainId: 98866
    minHealthFactor: 0.9
    maxHealthFactor: 1.1
    page: 0
  ) {
    totalItems
    page
    limit
    totalPages
    data {
      marketId
      borrower
      healthFactor
      liquidatablePosition
      positionWithBadDebt
      badDebt
      collateralToBeSeized
      debtToBeRepaid
      openPremium
      liquidationPenalty
    }
  }
}
```

**Liquidated Positions**

```graphql
query LiquidatedPositions {
  liquidatedPositions(chainId: 98866, page: 0) {
    totalItems
    totalPages
    data {
      marketId
      liquidator
      liquidatee
      collateralSeized
      debtRepaid
      premium
      profit
      timestamp
      transactionHash
    }
  }
}
```


# Mystic Swap API

Mystic Router is a leading DEX aggregator that finds the best prices across 100+ liquidity sources on multiple chains.

|                                                     |                                                                                                                         |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Base URL**                                        | `https://router.mysticfinance.xyz`                                                                                      |
| **Interactive API docs**                            | [Swagger](https://router.mysticfinance.xyz/docs)                                                                        |
| **Machine-readable spec** (for AI agents / codegen) | [`/llm.txt`](https://router.mysticfinance.xyz/llm.txt)                                                                  |
| **Health**                                          | [`/health`](https://router.mysticfinance.xyz/health)                                                                    |
| **Runnable demos (frontend)**                       | [`demos/frontend`](https://github.com/mystic-finance/mystic-router-integration/tree/main/demos/frontend) (React + Vite) |
| **Runnable demos (backend)**                        | [`demos/backend`](https://github.com/mystic-finance/mystic-router-integration/tree/main/demos/backend) (Node.js)        |
| **Support**                                         | Contact the Mystic team at <joao.moreira@mysticfinance.xyz> for an API key and partner onboarding.                      |

***

### Contents

* Why Mystic
* Authentication
* Quickstart
* API reference
* Supported chains & coverage
* Fees
* Errors

***

### Why Mystic

Access the best swap rates, deep liquidity and reliable execution across 6+ chains and 100+ liquidity sources with a single API.

#### Core capabilities

* **Two-layer aggregation.** A first-party routing engine that prices pools directly from on-chain dexes, plus a meta-aggregator over every major external aggregator. Both are quoted in parallel on every request, so you get the best price possible in one single API.
* **Smart order routing.** Multi-hop and split routes across pools, ranked by net output rather than by whichever venue answered first, giving unified access to 100+ liquidity sources
* **Coverage where others are thin.** Mystic aggregates on the major EVM chains, plus chains the big aggregators serve poorly (Flare, Plume, Citrea), giving a unified interface for 12+ chains
* **Built-in monetisation.** Route swaps under your API key and earn a share of every fee. See Fees.

***

### Authentication

The API is **open**: quoting, building and tracking work with no credentials. An API key buys throughput and fee attribution.

#### Using a key

Send it as the `x-api-key` header (`Authorization: Bearer <key>` is also accepted) on your `quote` and `build` calls:

```js
const http = axios.create({
  baseURL: BASE,
  headers: { 'x-api-key': process.env.MYSTIC_API_KEY },
});

await http.post('/v1/swap/quote', {
  chainId, sellToken, buyToken, sellAmount, taker, slippageBps: 50,
});
```

* **With a valid key** → your revenue share is applied and attributed to you, and you get the higher rate limit. See Fees.
* **With an invalid key** → `401 Invalid API key`. (Omitting the key entirely is fine; sending a bad one is not.)
* **Without a key** → anonymous: the standard 0.15% fee, no attribution, rate limit of 20 requests per second.

You can also earn fees without a key at all, by passing a `referrer` address on the swap. See Referral fees.

#### Keeping your key safe

Your API key is a secret tied to your revenue share. Keep it server-side and proxy browser traffic through your own backend. CORS is open, so a key shipped to the frontend can be read and used by anyone. Keys can be rotated by the operator at any time, and partner accounts support an origin allowlist for browser-facing setups.

### Quickstart

Swap tokens in 6 steps:

1. Get token info
2. Get price quote
3. Build transaction from quote
4. Set a token allowance
5. Send transaction
6. Get transaction status (optional)

Steps 2 and 3 can be collapsed into a single call if you'd rather not hold a quote id, see step 3.

Two conventions before you start:

> **Amounts are integers in the token's smallest unit.** Every amount in every request and response is a decimal string in wei, never a float and never a human-readable number. `1 USDC` (6 decimals) is `"1000000"`; `1 WETH` (18 decimals) is `"1000000000000000000"`. Step 1 gets you the `decimals` to build it with.

> **Native assets** use the sentinel address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` as `sellToken` or `buyToken`. No wrapping on your side, and no approval needed when selling native.

All examples use JavaScript with **axios** for HTTP and **ethers.js** for chain interactions.

```js
import axios from 'axios';
import { ethers } from 'ethers';

const BASE = 'https://router.mysticfinance.xyz';
```

#### 1. Get token info

List the supported tokens for a chain (use this to populate a token picker):

```js
async function tokenList(chainId) {
  const { data } = await axios.get(`${BASE}/v1/tokens?chainId=${chainId}`);
  return data;
}
```

**Example response** (`GET /v1/tokens?chainId=14`):

```json
[
  { "chainId": 14, "address": "0x1d80c49bbbcd1c0911346656b529df9e5c2f783d", "symbol": "WFLR", "decimals": 18, "name": "Wrapped Flare", "coingeckoId": "wrapped-flare", "tags": [] },
  { "chainId": 14, "address": "0xfbda5f676cb37624f28265a144a48b0d6e87d3b6", "symbol": "USDC.e", "decimals": 6, "name": "Bridged USDC (Stargate)", "tags": ["stable"] }
]
```

When a user pastes an unknown asset address, resolve its on-chain metadata (this also adds it to the registry):

```js
async function resolveToken(chainId, address) {
  const { data } = await axios.get(`${BASE}/v1/tokens/resolve?chainId=${chainId}&address=${address}`);
  return data;
}
```

**Example response** (`GET /v1/tokens/resolve?chainId=14&address=0x1D80…783d`):

```json
{ "chainId": 14, "address": "0x1d80c49bbbcd1c0911346656b529df9e5c2f783d", "symbol": "WFLR", "decimals": 18, "name": "Wrapped Flare" }
```

> `resolveToken` returns metadata only, it does not tell you whether the token is tradeable. To check if there's a route for this pair, run the quote in Step 2: a result means tradeable; a `404 INSUFFICIENT_LIQUIDITY` means no available route. There is no separate pool-check endpoint, the quote is the check.

#### 2. Get price quote

Fan out across every DEX + aggregator and return routes ranked best-first (`quotes[0]` is the best):

```js
async function quote({ chainId, sellToken, buyToken, sellAmount, taker, slippageBps = 50 }) {
  const { data } = await axios.post(`${BASE}/v1/swap/quote`, {
    chainId, sellToken, buyToken, sellAmount, taker, slippageBps,
  });
  return data;
}
```

**Example response** (selling 10 WFLR for USDC.e on chain 14). The winning route is flattened onto the root, and `quotes` holds the full ranked list, best first:

```json
{
  "quoteSetId": "qs_58b12f0b-6a2e-4b0c-9f4e-1c2d3e4f5a6b",
  "partner": { "partnerId": "protocol", "feeBps": 15, "recipient": "0x0F44298b5C26259425f982F8Fe5eEE1C30FaBBe4" },
  "mevAdvice": { "protect": false },
  "chainId": 14,
  "inToken": { "address": "0x1d80c49bbbcd1c0911346656b529df9e5c2f783d", "symbol": "WFLR", "name": "Wrapped Flare", "decimals": 18 },
  "outToken": { "address": "0xfbda5f676cb37624f28265a144a48b0d6e87d3b6", "symbol": "USDC.e", "name": "Bridged USDC (Stargate)", "decimals": 6 },
  "inAmount": "10000000000000000000",
  "outAmount": "64199",
  "minOutAmount": "63878",
  "estimatedGas": 250000,
  "price_impact": "0.12%",
  "from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "to": "0x69D57B9D705eaD73a5d2f2476C30c55bD755cc2F",
  "value": "0",
  "adapterId": "algebra",
  "quoteId": "algebra::qs_58b12f0b-6a2e-4b0c-9f4e-1c2d3e4f5a6b",
  "quotes": [
    {
      "quoteId": "algebra::qs_58b12f0b-6a2e-4b0c-9f4e-1c2d3e4f5a6b",
      "adapterId": "algebra",
      "rank": 1,
      "venueName": "SparkDEX V4",
      "routeSummary": "Algebra (SparkDEX V4, dyn fee 500bps)",
      "sellAmount": "10000000000000000000",
      "buyAmount": "64199",
      "minBuyAmount": "63878",
      "priceImpactBps": 12,
      "estimatedGas": "250000",
      "partnerFeeBps": 15,
      "validUntil": 1782931426325
    }
  ]
}
```

`outAmount` is the expected output (`64199` = `0.064199` USDC.e, since USDC.e has 6 decimals); `minOutAmount` is the worst case after slippage. Both are reported before Mystic's fee, see Fees for the net-output formula.

To execute, keep the root `quoteSetId` and `quoteId` and pass both to Step 3. Read `quotes[]` only when you want to show alternatives or let the user pick a venue; set `bestOnly: true` to drop it from the response entirely.

Native asset in/out: use `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` as the token. Optional fields: `recipient` (send output elsewhere), `deadlineSeconds`, `minOutput`, `bestOnly`, `referrer`/`referrerFee`, `includeDexes`/`excludeDexes`, `partnerId`, `mevProtect`. See the API reference for all of them.

#### 3. Build transaction from quote

Turn the chosen quote into an unsigned transaction (and learn what approval it needs):

```js
async function swap({ quoteSetId, quoteId, userAddress }) {
  const { data } = await axios.post(`${BASE}/v1/swap/build`, { quoteSetId, quoteId, userAddress });
  return data;
}
```

**Example response** (data truncated for readability):

```json
{
  "quoteSetId": "qs_58b12f0b-6a2e-4b0c-9f4e-1c2d3e4f5a6b",
  "adapterId": "algebra",
  "feeMode": "augustus",
  "txRequest": {
    "chainId": 14,
    "to": "0x75FaCE9583A037bf0870Ef6D24f08e207D2CCdDc",
    "data": "0x54e3f31b0000000000000000000000000000000000000000000000000000000000000020…",
    "value": "0",
    "from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
  },
  "approval": {
    "token": "0x1d80c49bbbcd1c0911346656b529df9e5c2f783d",
    "spender": "0x6352B36E5f938C0FdA3BA8da48D5aD14f1DD78E7",
    "amount": "10000000000000000000"
  },
  "simulation": { "ok": true },
  "partner": { "partnerId": "protocol", "feeBps": 15, "protocolBps": 15, "partnerBps": 0, "partnerRecipient": null }
}
```

`txRequest` is what you send from the wallet (Step 5). `approval` tells you which token/spender to approve in Step 4, it's `null` when no approval is needed. If `feeMode` is `augustus`, the fee handling is already baked into `txRequest.data`; you don't need to add anything.

> **Approve the `approval.spender`, not `txRequest.to`.** They are usually different contracts. Approving the wrong address is the single most common integration bug.

**One-call swap.** If you don't want to hold a `quoteSetId`/`quoteId` between calls, send the swap parameters straight to `build`. It quotes, picks the winner and returns its transaction in one round trip:

```js
const { data } = await axios.post(`${BASE}/v1/swap/build`, {
  chainId: 14, sellToken: SELL_TOKEN, buyToken: BUY_TOKEN, sellAmount,
  userAddress: taker, slippageBps: 50,
});
// same response shape: data.txRequest, data.approval, data.partner
```

The trade-off is that you never see the ranked alternatives, and the price is fixed at build time rather than shown to the user first. Use the two-step flow when a human is confirming the trade.

#### 4. Set a token allowance

If `approval` is returned and the current allowance is insufficient, approve the `spender` (skip for native sells, or use `permit2` if present):

```js
async function ensureAllowance(signer, approval) {
  if (!approval) return;
  const erc20 = new ethers.Contract(
    approval.token,
    ['function allowance(address,address) view returns (uint256)', 'function approve(address,uint256) returns (bool)'],
    signer,
  );
  const owner = await signer.getAddress();
  if ((await erc20.allowance(owner, approval.spender)) < BigInt(approval.amount)) {
    await (await erc20.approve(approval.spender, approval.amount)).wait();
  }
}
```

#### 5. Send transaction

```js
async function send(signer, txRequest) {
  const tx = await signer.sendTransaction({
    to: txRequest.to, data: txRequest.data, value: BigInt(txRequest.value || '0'),
  });
  return tx.wait(); // receipt
}
```

`tx.wait()` resolves once the transaction is mined. The receipt you get back from ethers looks like:

```json
{
  "hash": "0x9c1f…4e7a",
  "status": 1,
  "blockNumber": 39218844,
  "gasUsed": "142318"
}
```

> `status: 1` means success, `status: 0` means the transaction reverted. `hash` is what you register in Step 6.

**Gas.** `txRequest` carries no `gas` field on purpose. Let your wallet or provider run `eth_estimateGas` on it. The `estimatedGas` on a quote is a ranking input, not a gas limit; if you set a limit from it, add a buffer of 1.25×–2.5×, or the transaction may run out of gas on a route whose real cost differs from the estimate. To price the fee yourself, or to show the user a speed choice, read `GET /v1/gas-price`.

#### 6. Get transaction status (optional)

Read a swap's status back from the API. Nothing needs to be registered first: a Mystic swap carries a correlation id in its calldata, so the API recognises the hash and attaches it to the quote that produced it.

```js
async function status(hash) {
  const { data } = await axios.get(`${BASE}/v1/tx/${hash}`);
  return data;
}
```

**Example response** (`GET /v1/tx/0x9c1f…4e7a`):

```json
{
  "chainId": 14,
  "hash": "0x9c1f…4e7a",
  "status": "SUCCESS",
  "blockNumber": 39218844,
  "gasUsed": "142318",
  "from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
}
```

`status` is `SUCCESS` or `FAILED` once the receipt is on-chain. A hash the API hasn't indexed yet comes back as `{ hash, status: "UNKNOWN", adopting: true }`; poll again shortly. Referral and partner fees are booked from the on-chain event, not from this call, so a spoofed or mismatched hash can't record a fee.

#### Putting it all together

```js
async function doSwap() {
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  const chainId = 14, taker = await signer.getAddress();

  // 1. token info (get decimals to build the amount)
  const { decimals } = await resolveToken(chainId, SELL_TOKEN);
  const sellAmount = ethers.parseUnits('10', decimals).toString();

  // 2. quote
  const q = await quote({ chainId, sellToken: SELL_TOKEN, buyToken: BUY_TOKEN, sellAmount, taker });
  const best = q.quotes[0]; // 404 here = no route for this pair

  // 3. transaction body
  const built = await swap({ quoteSetId: q.quoteSetId, quoteId: best.quoteId, userAddress: taker });

  // 4. allowance  5. send  6. status
  await ensureAllowance(signer, built.approval);
  const receipt = await send(signer, built.txRequest);
  console.log(await status(receipt.hash));
}
```

***

### API reference

All business routes are versioned under `/v1`. Request and response bodies are JSON.

#### `POST /v1/swap/quote`

Fans out across every routing source available for the chain and returns them ranked.

**Request**

| Field                   | Type      | Required | Description                                                                                                                                                          |
| ----------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chainId`               | integer   | ✅        | Target chain. See Supported chains & coverage.                                                                                                                       |
| `sellToken`             | string    | ✅        | ERC-20 address, or `0xEeee…EEeE` for native.                                                                                                                         |
| `buyToken`              | string    | ✅        | ERC-20 address, or `0xEeee…EEeE` for native.                                                                                                                         |
| `sellAmount`            | string    | ✅        | Integer string in the smallest unit. Must be > 0.                                                                                                                    |
| `taker`                 | string    | ✅        | Wallet that signs and sends the swap.                                                                                                                                |
| `slippageBps`           | integer   | —        | Basis points; `50` = 0.5%. Range `0`–`5000`. Default `50`.                                                                                                           |
| `recipient`             | string    | —        | Where the bought token is delivered. Defaults to `taker`. See the note below this table.                                                                             |
| `deadlineSeconds`       | integer   | —        | Execution deadline encoded into the route. Range `60`–`86400`.                                                                                                       |
| `minOutput`             | string    | —        | Hard floor on the bought amount, in base units. Routes whose guaranteed minimum can't meet it are dropped instead of quoted. Overrides the slippage-derived minimum. |
| `includeDexes`          | string\[] | —        | Restrict the fan-out to these venues, by the `id` values from `GET /v1/dexes`.                                                                                       |
| `excludeDexes`          | string\[] | —        | Quote every venue except these.                                                                                                                                      |
| `includeAdapters`       | string\[] | —        | Same idea at adapter granularity, using the `adapterId` values quotes report (e.g. `uniswap-v3`, `1inch`). Merged with the dex filters.                              |
| `excludeAdapters`       | string\[] | —        | Quote everything except these adapters.                                                                                                                              |
| `referrer`              | string    | —        | Any EOA you control. Identifies you as the fee payee with no API key and no registration. Pair with `referrerFee`.                                                   |
| `referrerFee`           | number    | —        | Total fee to charge, as a **percent** (`1` = 1%), range `0.01`–`5`. Supersedes whatever fee your API key would have applied. See Referral fees.                      |
| `bestOnly`              | boolean   | —        | Return only the flattened winner and omit the `quotes` array. The top-level `quoteId` still builds it.                                                               |
| `mevProtect`            | boolean   | —        | Ask for MEV-aware routing. The response's `mevAdvice` reports whether a private RPC is available for the chain.                                                      |
| `useSmartAccount`       | boolean   | —        | Caller settles through a smart account, which lets the first-party engine offer atomic cross-pool split routes.                                                      |
| `partnerId`             | string    | —        | Usually set implicitly via your API key. Passing it explicitly can only request the same or a lower fee.                                                             |
| `partnerFeeBpsOverride` | integer   | —        | Request a lower fee than the default  fee. Rejected with `FEE_VIOLATION` if above the platform cap.                                                                  |

**Response**

The winning route is flattened onto the root of the response, so you can read the numbers without walking into `quotes[0]`. Both views describe the same route.

| Field                  | Type      | Description                                                                                                                                                                                                              |
| ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `quoteSetId`           | string    | `qs_…`, identifies this fan-out. Pass to `build`. Retrievable for \~2 minutes.                                                                                                                                           |
| `quoteId`              | string    | The winning route's id. Pass to `build`.                                                                                                                                                                                 |
| `adapterId`            | string    | Source that produced the winner.                                                                                                                                                                                         |
| `chainId`              | integer   | Echo of the request.                                                                                                                                                                                                     |
| `inToken` / `outToken` | object    | `{ address, symbol, name, decimals }` for each side.                                                                                                                                                                     |
| `inAmount`             | string    | Input amount in base units.                                                                                                                                                                                              |
| `outAmount`            | string    | Expected output in base units, gross of the Mystic fee.                                                                                                                                                                  |
| `minOutAmount`         | string    | Worst-case output after slippage, gross of the fee.                                                                                                                                                                      |
| `estimatedGas`         | number    | Gas estimate for the winning route.                                                                                                                                                                                      |
| `price_impact`         | string    | Price impact as a percentage string, e.g. `"0.12%"`.                                                                                                                                                                     |
| `from`                 | string    | The `taker` you sent.                                                                                                                                                                                                    |
| `to`                   | string    | Contract the swap will call.                                                                                                                                                                                             |
| `value`                | string    | Native value the transaction carries, in wei.                                                                                                                                                                            |
| `data`                 | string    | Calldata, when the winning source produced it at quote time. Call `build` for the authoritative transaction.                                                                                                             |
| `partner.partnerId`    | string    | `protocol` when anonymous, `referrer:0x…` when using a referrer, otherwise your partner id.                                                                                                                              |
| `partner.feeBps`       | integer   | **Total** fee in bps charged on this swap, in the bought token.                                                                                                                                                          |
| `partner.recipient`    | string    | On-chain fee collection address.                                                                                                                                                                                         |
| `dexFilter.notHonored` | string\[] | Present only when a `includeDexes`/`excludeDexes` id could not be applied, either because it's unknown or because filtering it would have caught sibling venues on the same adapter. Filters are never applied silently. |
| `quotes[]`             | array     | Ranked routes, best first. Omitted when `bestOnly` is set.                                                                                                                                                               |

**Quote object** (each entry of `quotes[]`)

| Field                   | Type           | Description                                                                                                                                    |
| ----------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `quoteId`               | string         | `<adapterId>::<quoteSetId>`. Pass to `build`.                                                                                                  |
| `adapterId`             | string         | Identifier of the source that produced the route. Also what `includeAdapters` / `excludeAdapters` take.                                        |
| `rank`                  | integer        | 1 = best.                                                                                                                                      |
| `tier`                  | string         | `tier-1` marks a route from Mystic's own pathfinder (`mystic-tier1`); every other source, including the direct-DEX adapters, reports `tier-2`. |
| `venueName`             | string         | Real DEX brand for display, e.g. `SparkDEX V3.1`, `Rooster Finance`.                                                                           |
| `routeSummary`          | string         | Human-readable path description.                                                                                                               |
| `fillType`              | string         | `single` or `split` (route divided across pools). Populated by the pathfinder.                                                                 |
| `route[]`               | array          | Machine-readable breakdown: `{ protocol, dexId, tokens[], portionBps, pool }`. Populated by the pathfinder.                                    |
| `sellAmount`            | string         | Echo of the requested input amount.                                                                                                            |
| `buyAmount`             | string         | Expected output, **gross of the Mystic fee**.                                                                                                  |
| `minBuyAmount`          | string         | Worst-case output after `slippageBps`, gross of the fee.                                                                                       |
| `priceImpactBps`        | integer        | Estimated price impact in bps. Evaluating and surfacing this is the integrator's responsibility; Mystic does not block high-impact trades.     |
| `estimatedGas`          | string         | Gas estimate used for ranking, not a gas limit. See step 5.                                                                                    |
| `estimatedGasUsd`       | number         | Gas cost in USD, when pricing is available.                                                                                                    |
| `estimatedAmountOutUsd` | number         | Output value in USD, when pricing is available.                                                                                                |
| `partnerFeeBps`         | integer        | Fee applied to this route.                                                                                                                     |
| `partnerFeeApplied`     | string         | `native` (the venue skims it) or `pending` (applied at build time).                                                                            |
| `validUntil`            | integer        | Epoch ms. Building after this returns `410 QUOTE_EXPIRED`.                                                                                     |
| `approvalTarget`        | string         | Spender this route would need. The authoritative value comes from `build`.                                                                     |
| `permit2`               | object \| null | Permit2 typed data, when the route supports approval-free spending.                                                                            |
| `warnings[]`            | string\[]      | Route-specific advisories, when present.                                                                                                       |
| `score`                 | number         | Internal ranking score. Informational.                                                                                                         |
| `raw`                   | object         | Opaque adapter payload. Internal, do not depend on its shape.                                                                                  |

Returns **404 `INSUFFICIENT_LIQUIDITY`** when no source can fill the trade.

**Quote lifetime.** A quote set stays retrievable for about two minutes, and each quote carries its own `validUntil` (epoch ms). Nothing is reserved or locked by quoting, so quote as often as you need. Build before `validUntil`, or you'll get `410 QUOTE_EXPIRED` and have to re-quote.

**Recipient.** Set `recipient ≠ taker` to deliver the bought token to a different address. Only sources that can honor a distinct recipient are offered for such a request, so funds never land on the taker by accident. If that filter leaves nothing routable you'll get `404 INSUFFICIENT_LIQUIDITY`; retry without `recipient` and transfer separately.

#### `POST /v1/swap/quote/reverse`

The buy flow: state the output you want and get back the input required, along with a real quote you can build. Use it for "I want exactly 100 USDC" rather than "I want to sell 1 ETH".

**Request**

| Field             | Type      | Required | Description                                                                                          |
| ----------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `chainId`         | integer   | ✅        | Target chain.                                                                                        |
| `sellToken`       | string    | ✅        | Token being sold.                                                                                    |
| `buyToken`        | string    | ✅        | Token being bought.                                                                                  |
| `buyAmount`       | string    | ✅        | Desired **output**, in the buy token's base units.                                                   |
| `taker`           | string    | ✅        | Wallet that signs and sends the swap.                                                                |
| `slippageBps`     | integer   | —        | Basis points; `50` = 0.5%. Range `0`–`5000`. Default `50`.                                           |
| `toleranceBps`    | integer   | —        | How close the solved output must land to `buyAmount`, in bps. Range `1`–`1000`. Default `50` (0.5%). |
| `recipient`       | string    | —        | Where the bought token is delivered. Defaults to `taker`.                                            |
| `includeDexes`    | string\[] | —        | Venue filter, as on `quote`.                                                                         |
| `excludeDexes`    | string\[] | —        | Venue filter, as on `quote`.                                                                         |
| `includeAdapters` | string\[] | —        | Adapter filter, as on `quote`.                                                                       |
| `excludeAdapters` | string\[] | —        | Adapter filter, as on `quote`.                                                                       |
| `referrer`        | string    | —        | Referral payee, as on `quote`.                                                                       |
| `referrerFee`     | number    | —        | Total fee percent, as on `quote`.                                                                    |
| `partnerId`       | string    | —        | Usually implicit via API key.                                                                        |

**Response**

Everything a normal quote returns, including `quoteSetId` and `quoteId`, so you can build the result directly. On top of that:

| Field                        | Type    | Description                                        |
| ---------------------------- | ------- | -------------------------------------------------- |
| `reverse.requestedBuyAmount` | string  | The output you asked for.                          |
| `reverse.achievedBuyAmount`  | string  | The output the solved input actually produces.     |
| `reverse.sellAmount`         | string  | The input required. This is the number you wanted. |
| `reverse.offByBps`           | integer | Distance between requested and achieved, in bps.   |
| `reverse.withinTolerance`    | boolean | Whether `offByBps` came in under `toleranceBps`.   |
| `reverse.passes`             | integer | How many quote passes the solver used, up to 3.    |

The solver quotes forward, measures the rate and corrects, up to three passes. AMM pricing is non-linear, so the result is an approximation: check `withinTolerance` before treating the input as exact, and remember the quote's own `minOutAmount` still applies at execution. Returns `404 INSUFFICIENT_LIQUIDITY` when no input size produces output for the pair.

#### `POST /v1/swap/build`

Two ways to call it. Either pass the ids from a quote, or pass the swap parameters and let `build` quote and pick the winner itself.

**Request, from a quote**

| Field         | Type   | Required | Description                                                                                                                                |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `quoteSetId`  | string | ✅        | From the quote response.                                                                                                                   |
| `quoteId`     | string | ✅        | The chosen route.                                                                                                                          |
| `userAddress` | string | ✅        | Wallet that will send the transaction.                                                                                                     |
| `recipient`   | string | —        | Overrides the quote's recipient. Defaults to the quote's recipient, then `userAddress`.                                                    |
| `referrer`    | string | —        | Accepted, but the value used is the one from the original quote's request, so a build can never change the payee the swap was quoted with. |
| `referrerFee` | number | —        | Accepted, but the value used is the one from the original quote's request, so a build can never raise the fee the user was quoted.         |

**Request, without a quote**

Omit **both** `quoteSetId` and `quoteId` and send the swap parameters instead. `build` runs the fan-out and builds the best route in the same call.

| Field             | Type      | Required | Description                                                                    |
| ----------------- | --------- | -------- | ------------------------------------------------------------------------------ |
| `userAddress`     | string    | ✅        | Wallet that will send the transaction, and the taker for the internal quote.   |
| `chainId`         | integer   | ✅        | Target chain.                                                                  |
| `sellToken`       | string    | ✅        | ERC-20 address, or `0xEeee…EEeE` for native.                                   |
| `buyToken`        | string    | ✅        | ERC-20 address, or `0xEeee…EEeE` for native.                                   |
| `sellAmount`      | string    | ✅        | Integer string in the smallest unit.                                           |
| `slippageBps`     | integer   | —        | Basis points; `50` = 0.5%. Range `0`–`5000`. Default `50`.                     |
| `minOutput`       | string    | —        | Hard floor on the bought amount, in base units.                                |
| `includeDexes`    | string\[] | —        | Restrict the fan-out to these venues, by the `id` values from `GET /v1/dexes`. |
| `excludeDexes`    | string\[] | —        | Quote every venue except these.                                                |
| `includeAdapters` | string\[] | —        | Restrict the fan-out to these adapters.                                        |
| `excludeAdapters` | string\[] | —        | Quote everything except these adapters.                                        |
| `referrer`        | string    | —        | Any EOA you control. Identifies you as the fee payee. Pair with `referrerFee`. |
| `referrerFee`     | number    | —        | Total fee to charge, as a **percent** (`1` = 1%), range `0.01`–`5`.            |
| `recipient`       | string    | —        | Where the bought token is delivered. Defaults to `userAddress`.                |

Missing parameters return `404` naming which ones are absent.

**Response**

| Field             | Type           | Description                                                                                                                                                                                                         |
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quoteSetId`      | string         | Echo.                                                                                                                                                                                                               |
| `adapterId`       | string         | Source that built the transaction.                                                                                                                                                                                  |
| `txRequest`       | object         | `{ chainId, to, data, value, from }`. Send this. `value` is a decimal string in wei.                                                                                                                                |
| `approval`        | object \| null | `{ token, spender, amount }`. `null` when no ERC-20 approval is needed (native sells, or a Permit2 flow).                                                                                                           |
| `permit2`         | object \| null | Typed data to sign instead of approving, when the route supports it.                                                                                                                                                |
| `feeMode`         | string         | How the fee is collected: `augustus` (atomic, inside `txRequest.data`), `native` (the venue's own referral mechanism), `bundle` (smart-account bundle), `none`. In every case there is nothing extra for you to do. |
| `partner`         | object         | `{ partnerId, feeBps, protocolBps, partnerBps, partnerRecipient }`, the fee split for this swap.                                                                                                                    |
| `simulation`      | object         | Advisory pre-flight result. `{ ok: true }` by default.                                                                                                                                                              |
| `directTxRequest` | object         | The un-wrapped venue transaction, before Mystic's fee wrapper. Informational: send `txRequest`, not this.                                                                                                           |

Returns **404** for an unknown `quoteSetId`/`quoteId`, **410 `QUOTE_EXPIRED`** for a stale quote.

#### `GET /v1/swap/quote/:quoteSetId`

Re-read a quote set you already fetched (debugging, or picking a different route later without re-quoting). Returns the stored per-route documents. Individual quotes still expire on their own `validUntil`.

#### `GET /v1/tokens?chainId=`

Registry list for a chain: `[{ chainId, address, symbol, decimals, name, coingeckoId?, tags[] }]`. `tags` drives fee tiering (`stable`, `correlated`, `common`, `exotic`) and is useful for grouping in a picker.

#### `GET /v1/tokens/resolve`

| Param     | Required | Description                            |
| --------- | -------- | -------------------------------------- |
| `chainId` | ✅        | Chain to read from.                    |
| `address` | ✅        | Token address, or the native sentinel. |

Reads `symbol`/`decimals`/`name` on-chain and adds the token to the registry. A non-ERC-20 address resolves to `UNKNOWN`/`Unknown` with `decimals: 18` rather than erroring, so treat "unknown symbol" as a signal to warn the user, and rely on the quote to decide tradeability.

#### `GET /v1/chains`

Live chain support with the contracts Mystic uses:

```json
[
  {
    "chainId": 14,
    "name": "Flare",
    "shortName": "flr",
    "isEip1559": true,
    "multicall3": "0xcA11bde05977b3631167028862bE2a173976CA11",
    "permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
    "weth": "0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d",
    "uniswapV3": [ … ],
    "algebra": [ … ],
    "rollupParent": null
  }
]
```

#### `GET /v1/gas-price`

Current gas price in three speed tiers. You don't need this to swap, `txRequest` is signable as returned, but it's useful for showing a fee estimate or setting your own gas fields.

| Param     | Required | Description                                                                                                                                |
| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `chainId` | —        | One chain. Omit it to get every supported chain as an array; a chain whose RPC is unreachable is left out rather than failing the request. |

```json
{
  "chainId": 14,
  "isEip1559": true,
  "baseFeePerGas": "25000000000",
  "standard": { "legacyGasPrice": "26500000000", "maxPriorityFeePerGas": "1500000000", "maxFeePerGas": "51500000000", "waitTimeEstimate": 30 },
  "fast":     { "legacyGasPrice": "33125000000", "maxPriorityFeePerGas": "1875000000", "maxFeePerGas": "51875000000", "waitTimeEstimate": 15 },
  "instant":  { "legacyGasPrice": "39750000000", "maxPriorityFeePerGas": "2250000000", "maxFeePerGas": "52250000000", "waitTimeEstimate": 5 }
}
```

| Field                           | Type           | Description                                                                                                              |
| ------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `chainId`                       | integer        | Chain the reading is for.                                                                                                |
| `isEip1559`                     | boolean        | Which fields to use. `true`: send `maxFeePerGas` + `maxPriorityFeePerGas`. `false`: send `legacyGasPrice` as `gasPrice`. |
| `baseFeePerGas`                 | string \| null | Base fee of the latest block. `null` on legacy chains.                                                                   |
| `standard` / `fast` / `instant` | object         | The three tiers.                                                                                                         |
| `<tier>.legacyGasPrice`         | string         | Type-0 gas price.                                                                                                        |
| `<tier>.maxPriorityFeePerGas`   | string         | Tip. `"0"` on legacy chains.                                                                                             |
| `<tier>.maxFeePerGas`           | string         | Priority fee plus headroom over the current base fee.                                                                    |
| `<tier>.waitTimeEstimate`       | integer        | Rough seconds to inclusion. Indicative only, the mempool isn't sampled.                                                  |

All values are **wei strings**, the same units `txRequest` uses, so nothing needs converting before you sign. Readings are cached for a few seconds per chain, so polling is cheap.

#### `GET /v1/tx/:hash`

Status of a swap transaction.

| Field                    | Type    | Description                                         |
| ------------------------ | ------- | --------------------------------------------------- |
| `chainId`                | integer | Chain the transaction was sent on.                  |
| `hash`                   | string  | Transaction hash.                                   |
| `status`                 | string  | `SUCCESS` or `FAILED` once the receipt is on-chain. |
| `blockNumber`            | integer | Block it landed in.                                 |
| `gasUsed`                | string  | Gas consumed.                                       |
| `effectiveGasPrice`      | string  | Gas price actually paid.                            |
| `from`                   | string  | Sender.                                             |
| `to`                     | string  | Contract called.                                    |
| `quoteSetId` / `quoteId` | string  | The quote this transaction executed, once matched.  |
| `receipt`                | object  | Full receipt.                                       |

A hash that hasn't been indexed yet returns `{ hash, status: "UNKNOWN", adopting: true }`. The matching runs in the background, so poll again shortly rather than treating it as an error.

#### `GET /v1/dexes`

Every venue quotable on a chain, which is what `includeDexes` / `excludeDexes` filter on. Pass `?chainId=` for one chain, or omit it for all of them.

```json
[
  { "id": "sparkdex", "name": "SparkDEX V4", "chainId": 14, "type": "dex", "protocol": "algebra", "router": "0x69D57B9D705eaD73a5d2f2476C30c55bD755cc2F", "adapter": "algebra" },
  { "id": "openocean", "name": "openocean", "chainId": 14, "type": "aggregator", "adapter": "openocean" }
]
```

| Field      | Type    | Description                                                                                                          |
| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------- |
| `id`       | string  | Stable id to use in `includeDexes` / `excludeDexes`.                                                                 |
| `name`     | string  | Display name.                                                                                                        |
| `chainId`  | integer | Chain the venue is on.                                                                                               |
| `type`     | string  | `dex` for a liquidity venue Mystic routes through itself, `aggregator` for a third-party router it asks for a quote. |
| `protocol` | string  | Pool family, e.g. `uniswap-v3`, `algebra`, `curve`.                                                                  |
| `router`   | string  | Router contract, for first-party venues.                                                                             |
| `adapter`  | string  | Adapter that serves it, which is the granularity filtering operates at.                                              |

#### `POST /v1/swap/decode`

Explain a transaction's calldata before or after you send it.

| Field     | Type    | Required | Description                                                             |
| --------- | ------- | -------- | ----------------------------------------------------------------------- |
| `data`    | string  | ✅        | Transaction input data, `0x`-prefixed.                                  |
| `chainId` | integer | —        | The decoder is chain-agnostic; accepted to keep request shapes uniform. |

```json
{
  "kind": "augustus-simple-swap",
  "augustus": {
    "fromToken": "0x1d80…783d",
    "toToken": "0xfbda…d3b6",
    "beneficiary": "0xd8dA…6045",
    "partner": "0x0F44…BBe4",
    "feePercent": "15",
    "deadline": "1782931426",
    "uuid": "0x9a3f…"
  },
  "calls": [
    { "selector": "0x095ea7b3", "function": "approve", "to": "0x1d80…783d", "args": ["0x6352…78E7", "10000000000000000000"] },
    { "selector": "0xbc651188", "function": "exactInputSingle", "to": "0x69D5…cc2F", "args": [ … ] }
  ]
}
```

`kind` is `augustus-simple-swap` for a fee-wrapped Mystic swap, or `call` for a plain adapter call. An unrecognised selector comes back with `function: null` rather than failing, so a partially-recognised route still tells you what it can.

#### Service endpoints

| Route                 | Purpose                                                          |
| --------------------- | ---------------------------------------------------------------- |
| `GET /health`         | Liveness and dependency check.                                   |
| `GET /docs`           | Swagger UI (interactive, always current).                        |
| `GET /llm.txt`        | Compact machine-readable API contract for AI agents and codegen. |
| `GET /integration.md` | This guide, served by the API.                                   |
| `GET /metrics`        | Operational metrics snapshot.                                    |

***

### Supported chains & coverage

Mystic aggregates 100+ liquidity sources across 12 chains. `GET /v1/chains` returns each chain's metadata plus the Multicall3, Permit2 and WETH addresses Mystic uses there.

| Chain        | chainId | Native | Explorer            | Liquidity covered                                                                                                                                    |
| ------------ | ------- | ------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ethereum     | `1`     | ETH    | etherscan.io        | Uniswap v3 & forks, Curve, CoW Protocol · 0x, 1inch, ParaSwap, KyberSwap, OpenOcean, Odos, OKX, Unizen, Enso, LI.FI, Beam, Nordstern                 |
| Base         | `8453`  | ETH    | basescan.org        | Uniswap v3 & forks (BaseSwap V3), Algebra pools, CoW Protocol · 0x, 1inch, ParaSwap, KyberSwap, OpenOcean, Odos, OKX, Unizen, Enso, LI.FI, fly.trade |
| Arbitrum One | `42161` | ETH    | arbiscan.io         | Uniswap v3 & forks, Camelot V3, Curve, CoW Protocol · 0x, 1inch, ParaSwap, KyberSwap, OpenOcean, Odos, OKX, Unizen, Enso, LI.FI, fly.trade           |
| Flare        | `14`    | FLR    | flarescan.com       | SparkDEX V2 / V3.1 / V4, BlazeSwap, Enosys · KyberSwap, OpenOcean, LI.FI                                                                             |
| Plume        | `98866` | PLUME  | explorer.plume.org  | Rooster Finance, Uniswap v3 forks, Curve, Nest RWA mints · OpenOcean                                                                                 |
| Citrea       | `4114`  | cBTC   | explorer.citrea.xyz | Satsuma, Uniswap v3 forks · Fibrous, LI.FI                                                                                                           |

***

### Fees

#### Trading fee

Mystic charges a **flat 15 bps (0.15%)** on every swap, on every chain, taken in the **bought token** and collected on-chain into Mystic's fee contract at swap time.

Fees are priced by token category. Today all four categories carry the same rate:

| Category   | Rate  | Example                                                    |
| ---------- | ----- | ---------------------------------------------------------- |
| Stable     | 0.15% | USDC ↔ USDT                                                |
| Correlated | 0.15% | USDC ↔ ETH; ETH ↔ stETH                                    |
| Common     | 0.15% | Top 200 tokens by market cap (excluding stable/correlated) |
| Exotic     | 0.15% | All other token combinations                               |

> **These rates can change.** The tiers may be differentiated in future (cheaper stables, higher exotics). Always read `partner.feeBps` from the quote response and apply it dynamically rather than hardcoding `15`.

#### Referral fees

The fastest way to monetise, with no API key and no onboarding: pass an address you control as `referrer`, and the total fee you want charged as `referrerFee`.

```js
await axios.post(`${BASE}/v1/swap/quote`, {
  chainId, sellToken, buyToken, sellAmount, taker,
  referrer: '0xYourWallet',
  referrerFee: 1,            // percent, so 1 = 1%
});
```

* `referrerFee` is a **percent** and must be between `0` and `5`. Anything higher returns `400 FEE_VIOLATION`.
* It replaces the standard fee rather than adding to it. It is the whole fee charged on that swap, and it supersedes whatever an API key would have applied.
* You keep **85%** of what's collected and Mystic keeps the rest.
* The address is the identity. Nothing to register, and it keys the payout ledger the same way a partner does, so accruals settle through the same path.
* Referral inputs are read from the **quote** when you build from a `quoteId`, so a build can never change the payee or raise the fee the user was quoted.

Fee Collection is not immediate and is done in batches, it accumulates trades rbought by referrer or partner and disburses fees twice every hour.

#### Partner fees

Partners earn a cut of the fee on the swaps they route. The default arrangement is **revenue share at 75/25**: the user pays the standard 0.15% and you receive your share of it (11.25 bps), while Mystic keeps the other half. Attaching your key never makes a quote worse for your user.

You can choose either model when your account is provisioned:

| Model                       | Total charged to the user | You earn                                                           |
| --------------------------- | ------------------------- | ------------------------------------------------------------------ |
| **Revenue share** (default) | Unchanged, 0.15%          | Your configured percentage of the fee. Default **75%**, so 0.1125% |
| **Surcharge**               | 0.15% **+** your bps      | Your full bps, on top of Mystic's cut                              |

On the surcharge model your bps is capped by 100 bps (1%); requesting more returns `400 FEE_VIOLATION`. `partnerFeeBpsOverride` can only ever request less than your configured default, for a promotional pair or a fee-free campaign.

#### How you get paid

1. The **total** fee is collected on-chain into Mystic's fee contract at swap time, one collection whichever model you're on.
2. Your share is booked to an off-chain ledger once the swap confirms on-chain. Mystic's indexer reads the correlation id embedded in the swap event and matches it to the quote that produced it, so attribution needs no call from you and a spoofed hash books nothing.
3. Balances are settled to your payout wallet on the operator's settlement cycle.

You can start routing before you have a payout wallet. Fees accrue and are held until you set one, then become payable at the next settlement.

***

### Errors

Domain errors return a machine-readable body:

```json
{ "code": "INSUFFICIENT_LIQUIDITY", "message": "No adapter could fulfil the requested route" }
```

Branch on `code`, not on the message text.

| HTTP | `code`                   | Meaning                                                                             | What to do                                                 |
| ---- | ------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| 400  | —                        | Request validation failed (bad address, unknown field, out-of-range `slippageBps`). | Fix the request.                                           |
| 400  | `UNSUPPORTED_CHAIN`      | `chainId` isn't served.                                                             | Check `GET /v1/chains`.                                    |
| 400  | `UNSUPPORTED_TOKEN`      | Token rejected, or `sellAmount` ≤ 0.                                                | Fix the token or amount.                                   |
| 400  | `FEE_VIOLATION`          | Requested fee exceeds your cap, or unknown/inactive partner.                        | Lower `partnerFeeBpsOverride`, or check your account.      |
| 401  | —                        | Invalid API key.                                                                    | Check the key; omit it to fall back to anonymous.          |
| 404  | `INSUFFICIENT_LIQUIDITY` | No route, including the case where no source can honor your `recipient`.            | Try a different size or pair, or drop `recipient`.         |
| 404  | —                        | Unknown `quoteSetId`, `quoteId` or adapter.                                         | Re-quote.                                                  |
| 410  | `QUOTE_EXPIRED`          | The quote passed its `validUntil`.                                                  | Re-quote and rebuild.                                      |
| 429  | —                        | Rate limited.                                                                       | Back off using `Retry-After`.                              |
| 502  | `ADAPTER_*`              | An upstream venue failed.                                                           | Retry; the fan-out normally routes around this on its own. |
| 500  | —                        | Server error.                                                                       | Retry with backoff; contact the operator if it persists.   |

Request bodies are strictly validated: an unrecognised field returns `400` rather than being ignored, so send only the documented parameters.


# Plans & Pricing

The API is offered on the plans below. Access is enforced by the `x-api-key` gate described in here. All plans grant full access to every endpoint, plans differ on included usage, throughput, and support.

The API comes in two flavours, both covered by the same plan:

* [**Mystic Morpho REST API**](https://docs.mysticfinance.xyz/overview/mystic-morpho-rest-api) — the REST endpoint to get Markets, Vaults, History and Liquidation data
* [**Mystic Morpho GraphQL API**](https://docs.mysticfinance.xyz/overview/mystic-morpho-graphql-api) — the GraphQL endpoint to get Markets, Vaults, History and Liquidation data

Plans are measured in credit allowances, and each credit is the unit of API usage. Each successful request costs 1 credit, be it a GraphQL query or a REST call (e.g. `/morphoCache/lite`, `/liquidations/positions`). Heavier aggregated endpoints may cost more than 1 credit. Failed requests (`4xx`/`5xx`) are not charged.

Each plan includes a monthly credit allowance that resets at the start of every billing cycle; usage beyond the allowance is billed at the Pay-as-you-go rate ($0.002 / credit). On the Pay-as-you-go plan you are billed only for what you use, with no monthly commitment.

<table><thead><tr><th width="106.296875">Plan</th><th width="101.234375">Price</th><th width="143.96875">Credits</th><th width="116.046875">Rate Limit</th><th>Best For</th></tr></thead><tbody><tr><td>Pay-as-you-go</td><td>$0.002 / credit</td><td>pay for usage</td><td>Standard</td><td>Testing, variable workloads, full API access with no monthly commitment.</td></tr><tr><td>Analyst</td><td>$149 / month</td><td>250k / month</td><td>20 requests / second</td><td>Researchers, data teams, DeFi users.</td></tr><tr><td>Builder</td><td>$299 / month</td><td>500k / month</td><td>50 requests / second</td><td>Liquidators, DeFi protocols.</td></tr><tr><td>Growth</td><td>$499 / month</td><td>1M / month</td><td>100 requests / second</td><td>DeFi protocols, Curators.</td></tr><tr><td>Enterprise</td><td>Custom</td><td>Custom</td><td>Custom</td><td>Foundations, wallets, CEXes.</td></tr></tbody></table>

**How API keys work**

* Send your API key on every request as `x-api-key`&#x20;
* Paid plans are billed monthly; an expired key returns `401` until renewed.
* Keys are issued and renewed by the Mystic team, [**contact us**](mailto:joao.moreira@mysticfinance.xyz) to choose a plan and get one.


# Supply

Users can supply their assets to Mystic and earn interest on them over time, as they contribute to the protocol’s liquidity. Here’s how to supply on Mystic:

* Head over to "Earn" tab of the "Vaults" page. It is the first page you see when you open the app. There, you will see several vaults curated by professional risk managers in DeFi.
* Choose one of the vaults you are interested in supplying to. In the vault page, Click “supply” and enter your preferred amount. You will see the interest rate you will be earning on a yearly basis.
* After supplying, you will receive in your wallet a token that represents your position in the pool and accrues the corresponding interest, dubbed "receipt tokens".
* You can, at any moment, withdraw your assets and the accrued interest from the pool (as long as there is sufficient unborrowed liquidity). Your receipt tokens are burned when you do, which triggers the corresponding amount plus interest to be sent to your wallet.

An important thing to note is that the interest rate you’re earning over time is not fixed, rather it depends on the utilization rate of the pool (how much of the supplied liquidity has been borrowed). When utilization is low, APR decreases. When utilization is high, APR increases.

Also, do not confuse supplying assets to earn yield (i.e. on the vault page) with supplying collateral so you can borrow (i.e. on the market page). To earn yield, you must supply the vault's borrow asset on the vault's page (e.g. pUSD, ETH, wPLUME), which has the title of the vault (e.g. "Mystic MEV Capital pUSD").


# Borrow

Users can borrow against what they have supplied to the protocol and pay an interest for doing so. The supplied amount, used as collateral, must be higher than the amount borrowed in a proportion specific to each market. This is to account for fluctuations in the asset’s value. This proportion is called the loan-to-value (LTV), which expresses the percentage of the collateral amount that can be borrowed against.

Here’s how to borrow on Mystic:

* Head over to the pool or vault you are interested in borrowing from.
* Click “borrow” and enter the collateral you’d like to supply and the amount you’d like to borrow. You will see the APR you will be paying, as well as your total debt amount.
* After borrowing, you will receive in your wallet a token that represents your loan's debt, aka a "loan token". This token will be increasing over time, to reflect your accumulating interest.

Over time, your position will accumulate debt and its health will deteriorate. To best manage it, you have to repay parts of your debt over time, which can be done at any moment. Repayments include the principal amount borrowed plus the accrued interest. Once a position has been fully repaid, the respective collateral is unlocked and can be withdrawn.


# Lending

Lenders supply assets to a pool (e.g. DAI, USDC) by depositing them in the protocol. For this service, they gain an interest on the assets they supply (i.e. the supply rate). These assets are then made available for borrowers, who both pay an interest as payment and put up tokenized assets of their own as collateral to be able to borrow assets supplied by lenders.

When users lend to the protocol, the earn receipt tokens. These tokens are yield-bearing, as they are a vehicle for Mystic to distribute interest to lenders. Suppliers share the interest paid by borrowers of their assets corresponding to the average borrow rate times the utilisation rate.


# Borrowing

Borrowers provide collateral worth more than what they are asking to borrow, to incentivize them to pay back the debt. The borrow rate (e.g. interest paid to borrow) depends on the utilization rate for each asset, and increases as utilization increases too. The utilization rate is the percentage of the asset's pool that has been borrowed already.

To note that borrower assets keep earning yield while deposited, which helps borrowers offset interest paid to lenders.

As time goes on, the interest due on a particular position increases and so, the health factor associated with that position decreases. Although borrowers can theoretically maintain their position for an undefined amount of time, their decreasing health factor can result in a liquidation.

Borrowers are encouraged to monitor their positions to avoid liquidations, which happen when the borrower's health factor goes under a certain level. To avoid this, borrowers can either repay the loan or deposit more assets to increase their health factor. See how health factor is calculated [here](https://docs.aave.com/risk/asset-risk/risk-parameters#health-factor).


# Liquidations

As a position accumulates interest, its ratio between collateral and amount owed decreases over time. Eventually, it will fall under the minimum collateralization rate needed for said market, making it eligible for liquidation. This happens either because the debt has increased past a certain threshold or because the collateral value has fluctuated.

Should a borrower need to be liquidated, some of their collateral is sold off to pay back the debt. Liquidations happen in different ways on Mystic depending on how liquid the collateral is:

* Liquid - these are assets that can be traded on DEX and are thus able to be liquidated the DeFi-native way: liquidation bots. In this instance, a bot will repay the borrowed amount and receive the respective collateral plus a liquidation premium. Anyone can run liquidation bots, with Mystic itself acting as the protocol’s first liquidator.
* Semi-liquid - these tokens can be redeemed for stablecoins with their issuer, but otherwise have no secondary market liquidity. In this case, Mystic is always the liquidator, and we redeem the assets with the issuer before repaying the debt to the lenders ourselves. These liquidations range from immediate to T+2, depending on the asset issuer’s redemption process.
* Illiquid - there is no way to get liquidity from the asset. In this case, the liquidation follows the process described in structured vaults - first collateral auction and then safety module slashing.

It is important to note that liquidations on Mystic respect the underlying asset's permissions, meaning only a whitelisted user can take permissioned liquidated collateral.

**Bad debt**

In a situation where there is extreme volatility, a market's LTV might increase too quickly before it can be liquidated. In such a scenario, the borrower defaults and liquidators are not incentivized to honour the debt, and so the protocol gets bad debt. Bad debt is less likely in Mystic Finance than in other money markets, due to the stability of the underlying assets we make markets for. However, should a bad debt event occur, some lenders may not be able to withdraw their assets.


# Loan-to-value

Mystic adjusts loan-to-Value (LTV) ratios per pool/asset in order to meet the different risk profiles of the underlying assets. This customization enables us to feature the best LTVs in every market. This allows users to borrow more against their collateral compared to other platforms, effectively maximizing capital efficiency.

### **Benefits of High LTV**

A high LTV ratio means that users can access more liquidity without needing to provide excessive collateral. This is most beneficial for those who want to leverage their investments or need quick access to capital for other opportunities. By offering competitive LTV ratios, Mystic Finance makes its platform more attractive to borrowers, increasing overall liquidity and RWA utility.

### **Risk Management**

While high LTV ratios provide significant benefits, they also require robust risk management practices. Mystic Finance employs several mechanisms to mitigate the risks associated with high LTV borrowing, including real-time collateral monitoring and insurance funds to cover potential bad debt on the pools. All LTV ratios are set together with our risk partners. Learn more in [Risk Management](/core-concepts/risk-management).


# Risk Management

Risk management is an essential component to any lending market. Mystic cares deeply about its users, and so we want to ensure we bring the best risk frameworks possible to the table. This includes, but is not limited to:

* Overcollateralization - All loans are overcollateralized proportional to the underlying asset's risk profile. The health of the loan is managed constantly and updated in real-time.
* Professional risk management - Mystic works with professional risk managers to carefully monitor and manage risk on the protocol to ensure users are taken care of.


# Addresses

{% tabs %}
{% tab title="Plume Mainnet" %}

| Contract                     | Address                                                                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Pool                         | [0xCE192A6E105cD8dd97b8Dedc5B5b263B52bb6AE0](https://phoenix-explorer.plumenetwork.xyz/address/0xCE192A6E105cD8dd97b8Dedc5B5b263B52bb6AE0) |
| Pool Configurator            | [0x0c8cE3082ddA7b4CaaE6866f751c7C60A7Ec6de8](https://phoenix-explorer.plumenetwork.xyz/address/0x0c8cE3082ddA7b4CaaE6866f751c7C60A7Ec6de8) |
| Interest Rate Strategy       | [0xeD7F9Af764Cb9949b095A05f7ED78aE3A71d0697](https://phoenix-explorer.plumenetwork.xyz/address/0xeD7F9Af764Cb9949b095A05f7ED78aE3A71d0697) |
| Pool Address Provider        | [0x6A5F6b4F1c7B8AFa16a941D52C4d706210E9ed2f](https://phoenix-explorer.plumenetwork.xyz/address/0x6A5F6b4F1c7B8AFa16a941D52C4d706210E9ed2f) |
| Leverage                     | [0x9928b8bcC7DeA72cFb49958a14A971389fd178e6](https://phoenix-explorer.plumenetwork.xyz/address/0x9928b8bcC7DeA72cFb49958a14A971389fd178e6) |
| Transaction Bundler          | [0x53838C7bdaa0d5693F342f88c8D1567e58BdC7fa](https://phoenix-explorer.plumenetwork.xyz/address/0x53838C7bdaa0d5693F342f88c8D1567e58BdC7fa) |
| Bundler Core Markets Adapter | [0x968A22e4CEdfE0Dd5e1aE4C6541Fa73f28da2b9f](https://phoenix-explorer.plumenetwork.xyz/address/0x968A22e4CEdfE0Dd5e1aE4C6541Fa73f28da2b9f) |

{% endtab %}
{% endtabs %}


# Pool

## Introduction

The `Pool` smart contract is the protocol's main user-facing contract, as most user interactions with Mystic occur via the Pool contract. It exposes the liquidity management methods that can be invoked using either Solidity or Web3 libraries.

`Pool` is covered by a proxy contract and is owned by the `PoolAddressesProvider` of any given market. All admin functions are callable by the `PoolConfigurator` contract defined in the `PoolAddressesProvider`. By interacting with `Pool`, users can:

* Supply
* Withdraw
* Borrow
* Repay
* Enable/disable supplied assets as collateral
* Liquidate positions

## Methods

### Supply

` ``` `\
`function supply(`

&#x20;   `address asset,`

&#x20;   `uint256 amount,`

&#x20;   `address onBehalfOf,`

&#x20;   `uint16 referralCode`

`) public virtual override`\
` ``` `

This method supplies a certain amount of an asset into the protocol, minting the same amount of corresponding myTokens and transferring them to the `onBehalfOf` address. For example, if a user supplies 100 USDC and `onBehalfOf` address is the same as `msg.sender`, they will get 100 myUSDC in return.

When supplying, the `Pool` contract must have `allowance()` to spend funds on behalf of `msg.sender` for at least the amount for the asset being supplied. This can be done via the standard ERC20 \``approve()`\` method on the underlying token contract.

\
Input Parameters:

| Name         | Type    | Description                                                                                                                                                                                                                                                                                                                |
| ------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| asset        | address | The address of the underlying asset being supplied to the pool                                                                                                                                                                                                                                                             |
| amount       | uint256 | The amount of asset to be supplied                                                                                                                                                                                                                                                                                         |
| onBehalfOf   | address | The address that will receive the corresponding myTokens. This is the only address that will be able to withdraw the asset from the pool. This will be the same as msg.sender if the user wants to receive myTokens into their own wallet, or use a different address if the beneficiary of myTokens is a different wallet |
| referralCode | uint16  | Referral supply is currently inactive, you can pass 0                                                                                                                                                                                                                                                                      |

This code is used to register the integrator originating the operation, for potential rewards. 0 if the action is executed directly by the user, without any middle-men.

### Withdraw

` ``` `\
`function withdraw(`

&#x20;   `address asset,`

&#x20;   `uint256 amount,`

&#x20;   `address to`

`) public virtual override`\
` ``` `

This method withdraws an amount of underlying asset from the reserve, burning the equivalent myTokens owned. For example, if a user has 100 myUSDC and calls `withdraw()`, they will receive 100 USDC, burning the 100 myUSDC.

If user has any existing debt backed by the underlying token, then the maximum amount available to withdraw is the amount that will not leave the user's health factor < 1 after withdrawal.

When withdrawing to another address, `msg.sender` should have myTokens to they may be burned by `Pool`.<br>

Input Parameters:

| Name   | Type    | Description                                                                                                                                                                                                                |
| ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| asset  | address | The address of the underlying asset to withdraw, not the myToken                                                                                                                                                           |
| amount | uint256 | The underlying amount to be withdrawn (the amount supplied), expressed in wei units. Use type(uint).max to withdraw the entire aToken balance                                                                              |
| to     | address | The address that will receive the underlying asset. This will be the same as msg.sender if the user wants to receive the tokens into their own wallet, or use a different address if the beneficiary is a different wallet |

### Borrow

` ``` `\
`function borrow(`

&#x20;   `address asset,`

&#x20;   `uint256 amount,`

&#x20;   `uint256 interestRateMode,`

&#x20;   `uint16 referralCode,`

&#x20;   `address onBehalfOf`

`) public virtual override`\
` ``` `

This method allows users to borrow a specific amount of the reserve underlying asset, provided the borrower has already supplied enough collateral, or they were given enough allowance by a credit delegator on the corresponding debt token (myVariableLoanToken). For example, if a user borrows 100 USDC passing their own address as `onBehalfOf`, they will receive 100 USDC into their wallet and 100 variable debt tokens.

NOTE: If `onBehalfOf` is not the same as `msg.sender`, then `onBehalfOf` must have supplied enough collateral via `supply()` and have delegated credit to `msg.sender` via `approveDelegation()`.

Mystic's referral program isn't active at the moment, meaning you can pass 0 as `referralCode`. This program may be activated in the future.<br>

Input Parameters:

| Name             | Type    | Description                                                                                                                                                                                                          |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| asset            | address | The address of the underlying asset to borrow                                                                                                                                                                        |
| amount           | uint256 | The amount to be borrowed, expressed in wei units                                                                                                                                                                    |
| interestRateMode | uint256 | Should always be passed a value of 2 (variable rate mode)                                                                                                                                                            |
| referralCode     | uint16  | <p>Referral supply is currently inactive, you can pass </p><p>0</p>                                                                                                                                                  |
| onBehalfOf       | address | This should be the address of the borrower calling the function if they want to borrow against their own collateral, or the address of the credit delegator if the caller has been given credit delegation allowance |

### Repay

` ``` `

`function repay(`

&#x20;   `address asset,`

&#x20;   `uint256 amount,`

&#x20;   `uint256 interestRateMode,`

&#x20;   `address onBehalfOf`

`) public virtual override`

` ``` `<br>

Repays a borrowed amount on a specific reserve, burning the equivalent debt tokens owned. For example, if a user repays 100 USDC, the 100 variable debt tokens owned by the `onBehalfOf` address will be burned.

When repaying, the `Pool` contract must have allowance to spend funds on behalf of `msg.sender` for at least the amount for the asset you are repaying with. This can be done via the standard ERC20 `approve()` method on the underlying token contract. Please note you cannot call `repay()` multiple times in the same block.<br>

Input Parameters:

| Name             | Type    | Description                                                                                                                                                                                                                                                                                                 |
| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| asset            | address | The address of the borrowed underlying asset previously borrowed.                                                                                                                                                                                                                                           |
| amount           | uint256 | The amount to repay, expressed in wei units. Use type(uint256).max in order to repay the whole debt, ONLY when the repayment is not executed on behalf of a 3rd party. In case of repayments on behalf of another user, it's recommended to send an amount slightly higher than the current borrowed amount |
| interestRateMode | uint256 | Only available option is 2 (variableRateMode)                                                                                                                                                                                                                                                               |
| onBehalfOf       | address | The address of the user who will get their debt reduced/removed. This should be the address of the user calling the function if they want to reduce/remove their own debt, or the address of any other borrower whose debt should be removed                                                                |

### setUserUseReserveAsCollateral

` ``` `

`function setUserUseReserveAsCollateral(`

&#x20;   `address asset,`

&#x20;   `bool useAsCollateral`

`) public virtual override`\
` ``` `<br>

This method allows suppliers to enable/disable a specific supplied asset as collateral, as it sets the asset of `msg.sender` to be used as collateral or not. Do note, an asset in Isolation Mode can only be enabled to use as collateral if no other asset is already enabled to use as collateral.

The user won’t be able to disable an asset as collateral if they have an outstanding debt position which could be left with the Health Factor < `HEALTH_FACTOR_LIQUIDATION_THRESHOLD`.<br>

Input Parameters:

| Name            | Type    | Description                                                             |
| --------------- | ------- | ----------------------------------------------------------------------- |
| asset           | address | The address of the underlying asset supplied                            |
| useAsCollateral | bool    | True if the user wants to use the supply as collateral, false otherwise |

### liquidationCall

` ``` `

`function liquidationCall(`

&#x20;   `address collateralAsset,`

&#x20;   `address debtAsset,`

&#x20;   `address user,`

&#x20;   `uint256 debtToCover,`

&#x20;   `bool receiveAToken`

`) public virtual override`\
` ``` `<br>

This method liquidates a non-healthy position collateral-wise, meaning one with a Health Factor below 1. To expand on this - when the health factor of a position is below 1, the caller (liquidator) repays the `debtToCover` amount of debt of the user getting liquidated. This is part or all of the outstanding borrowed amount on behalf of the borrower. The caller then receives a proportional amount of the `collateralAsset` (discounted amount of collateral) plus a liquidation bonus to cover market risk.

Liquidators can decide if they want to receive an equivalent amount of collateral myTokens instead of the underlying asset. When the liquidation is completed successfully, the health factor of the position is increased, bringing the health factor above 1.

Liquidators can only close a certain amount of collateral defined by a close factor. Currently the close factor is 0.5. In other words, liquidators can only liquidate a maximum of 50% of the amount pending to be repaid in a position. The liquidation discount applies to this amount.

In most scenarios, profitable liquidators will choose to liquidate as much as they can (50% of the user position). If you wish to do this, the `debtToCover` parameter can be set to uint(-1) and the protocol will proceed with the highest possible liquidation allowed by the close factor. To check a user's health factor, use `[getUserAccountData()]`.<br>

Liquidators must `approve()` the `Pool` contract to use `debtToCover` of the underlying ERC20 of the asset  used for the liquidation.<br>

Input Parameters:

| Name            | Type    | Description                                                                                                                                                         |
| --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| collateralAsset | address | The address of the underlying asset used as collateral, to receive as result of the liquidation                                                                     |
| debtAsset       | address | The address of the underlying borrowed asset to be repaid with the liquidation                                                                                      |
| user            | address | The address of the borrower getting liquidated                                                                                                                      |
| debtToCover     | uint256 | The debt amount of borrowed asset the liquidator will repay                                                                                                         |
| receiveAToken   | bool    | true if the liquidator wants to receive the myTokens equivalent of the purchased collateral, false if they want to receive the underlying collateral asset directly |

### getUserAccountData

` ``` `

`function getUserAccountData(address user) external view virtual override returns (`

&#x20;   `uint256 totalCollateralBase,`

&#x20;   `uint256 totalDebtBase,`

&#x20;   `uint256 availableBorrowsBase,`

&#x20;   `uint256 currentLiquidationThreshold,`

&#x20;   `uint256 ltv,`

&#x20;   `uint256 healthFactor`

`)`

` ``` `

Returns the user account data across all the reserves.

Input Parameters:

| Name | Type    | Description             |
| ---- | ------- | ----------------------- |
| user | address | The address of the user |

Return Values:

| Name                        | Type    | Description                                                                      |
| --------------------------- | ------- | -------------------------------------------------------------------------------- |
| totalCollateralBase         | uint256 | The total collateral of the user in the base currency used by the price feed     |
| totalDebtBase               | uint256 | The total debt of the user in the base currency used by the price feed           |
| availableBorrowsBase        | uint256 | The borrowing power left of the user in the base currency used by the price feed |
| currentLiquidationThreshold | uint256 | The liquidation threshold of the user                                            |
| ltv                         | uint256 | The loan to value of the user                                                    |
| healthFactor                | uint256 | The current health factor of the user                                            |

### **getUserConfiguration**

` ``` `

`function getUserConfiguration(`

&#x20;   `address user`

`) external view virtual override`

`returns (`

&#x20;   `DataTypes.UserConfigurationMap memory`

`)`\
` ``` `

Returns the configuration of the user across all the reserves.<br>

Input Parameters:

| Name | Type    | Description      |
| ---- | ------- | ---------------- |
| user | address | The user address |

Return Values:

| Type                           | Description                   |
| ------------------------------ | ----------------------------- |
| DataTypes.UserConfigurationMap | The configuration of the user |

The `DataTypes.UserConfigurationMap` struct is composed of the following fields:

| Name | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data | uint256 | <p>Bitmap of the user's collaterals and borrows. It is divided into pairs of bits, one pair per asset. The first bit indicates if an asset is used as collateral by the user, the second whether an asset is borrowed by the user. The corresponding assets are in the same position as getReservesList(). For example, if the hex value returned is 0x40020, which represents a decimal value of 262176, then in binary it is </p><p>1000000000000100000. If we format the binary value into pairs, starting from the right, we get </p><p>1 00 00 00 00 00 00 10 00 00. If we start from the right and move left in the above binary pairs, the third pair is 10. Therefore the 1 indicates that third asset from the reserveList is used as collateral, and 0 indicates it has not been borrowed by this user</p> |

### getReservesList

` ``` `

`function getReservesList(`

`)external view virtual override`

`returns (`

&#x20;   `address[] memory`

`)`

` ``` `<br>

Returns the list of the underlying assets of all the initialized reserves. It does not include dropped reserves.<br>

Return Values:

| Type       | Description                                                        |
| ---------- | ------------------------------------------------------------------ |
| address\[] | The addresses of the underlying assets of the initialized reserves |


# Vaults (coming soon)

## Introduction

The `MysticVaults` contract is a core component of the Mystic Finance protocol, designed to manage asset allocation across multiple pools, as well as handle user deposits, withdrawals, and borrowing. Curators, who are responsible for optimizing liquidity, can add or remove assets and pools, adjust allocations, and manage liquidity flows in vaults.

The contract interacts with the Mystic `Pools` to support various liquidity strategies and complies with the ERC4626 standard for vaults, allowing seamless integrations with other DeFi protocols.

## Methods

### **addCurator**

`function addCurator(`

&#x20;   `address curator`

`) external;`

Curators in `MysticVaults` have the authority to manage assets, reallocate resources, and update pool configurations. These curators ensure that the vault’s allocations are optimized for risk-adjusted returns. This method adds a new curator who can manage asset allocations.

Input Parameters:

| Name    | Type    | Description                                                                                                          |
| ------- | ------- | -------------------------------------------------------------------------------------------------------------------- |
| curator | address | The address of the curator to be added. This address will gain the right to modify allocations and manage the vault. |

&#x20;

### **removeCurator**

`function removeCurator(`

&#x20;   `address curator`

`) external;`<br>

This method removes an existing curator and prevents them from managing the vault.

Input Parameters:

| Name    | Type    | Description                               |
| ------- | ------- | ----------------------------------------- |
| curator | address | The address of the curator to be removed. |

### **addMysticPool**<br>

`function addMysticPool(`

&#x20;   `address newAsset,`&#x20;

&#x20;   `address oracle,`&#x20;

&#x20;   `uint256 allocationPercentage,`&#x20;

&#x20;   `address mysticPoolAddress`

`) external;`<br>

This method adds a new Mystic Pool to the vault's portfolio.

Input Parameters:

| Name                 | Type    | Description                                                               |
| -------------------- | ------- | ------------------------------------------------------------------------- |
| newAsset             | address | The address of the new asset that will be allocated to this pool.         |
| oracle               | address | The address of the oracle that provides price data for the new asset.     |
| allocationPercentage | uint256 | The percentage of the vault’s assets that will be allocated to this pool. |
| mysticPoolAddress    | address | The address of the Mystic Pool to which the new asset will be allocated.  |

### **updateAssetAllocation**<br>

`function updateAssetAllocation(`

&#x20;   `address updateAsset,`&#x20;

&#x20;   `address mysticPoolAddress,`&#x20;

&#x20;   `uint256 newAllocationPercentage`

`) external;`<br>

This method updates the asset allocation for a specific pool.

Input Parameters:

| Name                    | Type    | Description                                                                  |
| ----------------------- | ------- | ---------------------------------------------------------------------------- |
| updateAsset             | address | The address of the asset to be reallocated.                                  |
| mysticPoolAddress       | address | The address of the pool for which the allocation will be updated.            |
| newAllocationPercentage | uint256 | The new percentage of the total assets to be allocated to the updated asset. |

### **reallocate**

`function reallocate(`

&#x20;   `address asset,`&#x20;

&#x20;   `address mysticPoolAddress,`&#x20;

&#x20;   `uint256 newAllocationPercentage`

`) external;`<br>

This method rebalances assets deposited between Mystic Pools.

Input Parameters:

| Name                    | Type    | Description                                                        |
| ----------------------- | ------- | ------------------------------------------------------------------ |
| asset                   | address | The address of the asset being reallocated.                        |
| mysticPoolAddress       | address | The address of the pool where the asset will be reallocated.       |
| newAllocationPercentage | uint256 | The new allocation percentage for the asset in the specified pool. |

### **maxDeposit**

`function maxDeposit(`

&#x20;   `address user) external view`

`returns (uint256);`<br>

This method returns the maximum amount a user can deposit in the vault.

Input Parameters:

| Name | Type    | Description                                    |
| ---- | ------- | ---------------------------------------------- |
| user | address | The address of the user attempting to deposit. |

### **maxWithdrawal**

`function maxWithdrawal(`

&#x20;   `address owner) external view`

`returns (uint256);`<br>

This method returns the maximum amount a user can withdraw from the vault.

Input Parameters:

| Name  | Type    | Description                                     |
| ----- | ------- | ----------------------------------------------- |
| owner | address | The address of the user attempting to withdraw. |

### **borrow**

`function borrow(`

&#x20;   `address collateralAsset,`&#x20;

&#x20;   `uint256 collateralAmount,`&#x20;

&#x20;   `uint256 amount,`&#x20;

&#x20;   `address mysticPoolAddress,`&#x20;

&#x20;   `address receiver,`&#x20;

&#x20;   `bool receiveShares`

`) external;`<br>

This method allows users to borrow against their collateral from a specific Mystic Pool.

Input Parameters:

| Name              | Type    | Description                                                                               |
| ----------------- | ------- | ----------------------------------------------------------------------------------------- |
| collateralAsset   | address | The address of the asset being used as collateral for the loan.                           |
| collateralAmount  | uint256 | The amount of the collateral asset to lock.                                               |
| amount            | uint256 | The amount of the asset to be borrowed.                                                   |
| mysticPoolAddress | address | The address of the Mystic Pool from which the asset will be borrowed.                     |
| receiver          | address | The address that will receive the borrowed asset.                                         |
| receiveShares     | bool    | Indicates whether the borrower wants to receive the vault shares or the underlying asset. |

### **withdraw**

`function withdraw(`

&#x20;   `uint256 amount,`&#x20;

&#x20;   `address receiver,`&#x20;

&#x20;   `address owner`

`) external returns (uint256);`<br>

This method withdraws a specified amount of assets from the vault.

Input Parameters:

| Name     | Type    | Description                                                     |
| -------- | ------- | --------------------------------------------------------------- |
| amount   | uint256 | The amount of the asset to be withdrawn.                        |
| receiver | address | The address that will receive the withdrawn asset.              |
| owner    | address | The address of the user withdrawing their asset from the vault. |

### **redeem**

`function redeem(`

&#x20;   `uint256 amount,`&#x20;

&#x20;   `address receiver,`&#x20;

&#x20;   `address owner`

`) external returns (uint256);`<br>

Redeems a specified amount of assets from the vault in exchange for vault tokens.

Input Parameters:

| Name     | Type    | Description                                       |
| -------- | ------- | ------------------------------------------------- |
| amount   | uint256 | The amount of the asset to redeem.                |
| receiver | address | The address that will receive the redeemed asset. |
| owner    | address | The address of the user redeeming the asset.      |

### **requestWithdrawal**

`function requestWithdrawal(`

`uint256 assets`

`) external;`

This method allows users to request a withdrawal, which is queued for processing.

Input Parameters:

| Name   | Type    | Description                                       |
| ------ | ------- | ------------------------------------------------- |
| assets | uint256 | The amount of assets the user wishes to withdraw. |

### **repay**

`function repay(`

&#x20;   `uint256 amount,`&#x20;

&#x20;   `address mysticPoolAddress,`&#x20;

&#x20;   `address onBehalfOf`

`) external;`

<br>

Repays a borrowed amount on behalf of a user, reducing their outstanding debt.

Input Parameters:

| Name              | Type    | Description                                               |
| ----------------- | ------- | --------------------------------------------------------- |
| amount            | uint256 | The amount to repay.                                      |
| mysticPoolAddress | address | The address of the pool from which the debt was borrowed. |
| onBehalfOf        | address | The address of the borrower whose debt is being repaid.   |

### **totalAssets**

`function totalAssets(`

`) external view returns (uint256);`

<br>

Returns the total amount of assets currently held in the vault.

### **totalAssetsInUsd**

`function totalAssetsInUsd(`

`) external view returns (uint256);`

Returns the total value of the assets in USD, based on the latest price data from oracles.


# Tutorials

## Introduction

Here you will find several tutorials on how you can do several actions on Mystic, namely:

1. Creating Vault
2. Supply and Withdraw (Core Pools or Vaults)
3. Borrow and Repay (Core Pools or Vaults)
4. Liquidation: Rates and Incentives
5. KYC
6. Custody

Without further ado, let's get stuck in!

### Creating Vault

1. Navigate to the Vault Factory: Access the vault factory contract on the chain you're using. The contract addresses can be found in the protocol’s "Addresses" section. The vault factory is necessary if you wish to act as a vault curator. For example, the vault factory for the Plume Network devnet can be found at 0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb.
2. Fill in the required attributes: Go to the createVault function within the Vault Factory contract, either through the explorer or the protocol UI, and fill in the necessary data:
   1. initialTimelock: Minimum time delay required for a withdrawal request to become withdrawable.
   2. asset: The primary asset for deposits and withdrawals (usually stablecoins like USDT, USDC).
   3. maxDeposit: The maximum deposit value for the asset.
   4. maxWithdrawal: The maximum withdrawal value for the asset.
   5. fee: The percentage fee for every withdrawal (e.g., 100 = 1%).
   6. feeRecipient: The address that will receive the fees.
   7. name: Name of the Vault and Vault token.
   8. symbol: Symbol of the Vault token.
   9. salt: A bytes32 value used in the deployment process (use 0x as the default).
3. Sign the transaction: Approve and sign the transaction in your wallet.
4. Retrieve the Vault Address: You can retrieve the vault address by:
   1. Checking the return value of the call (if using Solidity/JavaScript code), or checking the transaction hash in the explorer, and viewing the events tab to find the CreateVault event. The first address in the event is the vault address.
5. Add Pool to Vault: To activate the vault, you need to add pools. Use the addMysticPool function via the explorer or UI and provide the following:
   1. newAsset: The asset for the pool reserve (should match the main asset in the vault).
   2. oracle: The oracle address for the asset (same oracle can be reused).
   3. allocationPercentage: The percentage of deposits allocated to this pool (1% = 100).
   4. mysticPoolAddress: The pool address to add.
   5.
6. Sign the second transaction: Approve and sign the addMysticPool transaction in your wallet.

### Supply and Withdraw (Core Pools or Vaults)

#### **Supplying on Core Pools:**

1. Access Mystic's Core Pools: Navigate to the Mystic Finance UI, select the chain, and go to the Core Pools section.
2. Select Asset: Choose an asset from the available reserves to deposit.
3. Approve and Sign Transactions: Approve the asset and sign the supply transaction.
4. Check for myTokens: After the transaction, check the explorer for the myToken (LP tokens) you have received.

#### **Supplying on Vaults:**

1. Access the Vault: Go to the Vaults page in the Mystic Finance UI, select the appropriate chain, and navigate to the vault.
2. Deposit the Required Asset: Enter the deposit amount for the defined vault asset.
3. Approve and Sign the Transaction: Approve the asset and sign the deposit transaction.
4. Check for Vault Tokens: Check the explorer for your Vault tokens after the transaction is completed.

#### **Withdrawing on Core Pools:**

1. Access Core Mystic Pools: Navigate to the core pools section in the UI.
2. Select Asset to Withdraw: Choose an asset to withdraw and enter the amount.
3. Sign the Withdraw Transaction: Sign the withdrawal transaction and check your wallet for the received assets.

#### **Withdrawing on Vaults:**

1. Access the Vault: Navigate to the vault section in the UI.
2. Withdraw Required Asset: Enter the withdrawal amount for the vault asset.
3. Sign the Withdraw Transaction: Approve and sign the transaction and check your wallet for the withdrawn assets.

### Borrow and Repay (Core Pools and Vaults)

#### **Borrow on Core Pools:**

1. Access Core Mystic Pools: Go to the Mystic Finance UI, select the chain, and navigate to the Core Pools section.
2. Select Asset to Borrow: Choose an asset from the list of reserves, then provide collateral and specify the borrowing amount.
3. Approve and Sign the Transactions: Approve the collateral, set it as collateral (if needed), and sign the borrowing transaction.
4. Check for Debt Tokens: Check your wallet for borrowed assets and debt tokens.

#### **Borrow on Vault:**

1. Access the Vault: Go to the vault page in the UI and select an asset to borrow.
2. Borrowing Options: Enter the collateral amount and the amount to borrow. You can choose whether to receive the borrowed asset or Vault shares based on the borrow amount.
3. Approve and Sign the Transactions: Approve the collateral and sign the borrow transaction.
4. Check for Vault Tokens: Check your wallet for borrowed assets or Vault shares and debt tokens.

**Repay on Core Pools:**

1. Access Core Mystic Pools: Navigate to the repayment section in the UI.
2. Select Asset to Repay: Choose an asset to repay and enter the repayment amount.
3. Sign the Repay Transaction: Sign the repayment transaction and check your wallet for reduced debt and burnt debt tokens.

**Repay on Vault:**

1. Access the Vault: Navigate to the vault repayment section.
2. Repay Vault Asset: Enter the repayment amount, choosing whether to repay with the vault or another asset.
3. Sign the Repay Transaction: Approve and sign the repayment transaction and check for reduced debt and burnt debt tokens.

### Liquidation: Rates and Incentives

Liquidation in Mystic Finance is triggered when a user’s health factor falls below 1, making their account eligible for liquidation. Liquidators can repay up to 90% of the user’s debt in exchange for a portion of their collateral plus a liquidation incentive. This process is essential for maintaining the health and stability of the protocol.

#### **LiquidationCall Function**

The liquidationCall function can be executed by liquidators when a borrower's account is undercollateralized. Here’s a breakdown of the function parameters and how to use it:

Parameters for liquidationCall:

` ``` `\
`function liquidationCall(`

&#x20;   `collateralAsset address,`&#x20;

&#x20;   `debtAsset address,`&#x20;

&#x20;   `user address,`&#x20;

&#x20;   `debtToCover uint256,`&#x20;

&#x20;   `receiveAToken bool,`

`) external;`

Input Parameters:

| Name            | Type    | Description                                                                                                                                                                          |
| --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| collateralAsset | address | The address of the asset being used as collateral (e.g., USDC, DAI). This is the asset that the liquidator will receive as part of the liquidation process.                          |
| debtAsset       | address | The address of the oracle that The address of the asset the borrower owes (i.e., the debt being liquidated). This is the asset that the liquidator repays on behalf of the borrower. |
| user            | addres  | The percentage of the vault’s The address of the borrower whose position is being liquidated.                                                                                        |
| debtToCover     | uint256 | The amount of debt (in the debt asset) that the liquidator wants to cover. Liquidators can choose to cover up to 90% of the borrower’s outstanding debt.                             |
| receiveAToken   | bool    | A boolean flag indicating whether the liquidator wants to receive myTokens (representing interest-bearing assets) or the underlying collateral directly.                             |

#### Liquidating a loan

To first find which loan to liquidate, liquidators must carefully monitor borrowers' Health Factors, which is a measure of how close an account is to liquidation. A health factor below 1 means the account is undercollateralized and can be liquidated.

&#x20;                          **Health Factor = (Total Collateral \* LTV) / Total Debt**

For example, if a user has $10,000 in collateral and $8,000 in debt, with a loan-to-value (LTV) ratio of 80%, the health factor would be (10,000 \* 0.80) / 8,000 = 1.0.&#x20;

If the user’s collateral value falls or their debt increases, the health factor may drop below 1, making them eligible for liquidation. When the health factor is below 1, the liquidator can repay part of the borrower’s debt by calling liquidationCall. In return, the liquidator receives a portion of the borrower’s collateral, plus an added extra to incentivize them to liquidate in the first place. This incentive compensates the liquidator for the risk and cost of performing the liquidation. Let's go through an example:

1. Let’s assume a user (borrower) has a health factor of 0.9 due to a fictional fluctuation in DAI's price, making them eligible for liquidation. The user has:
   1. $10,000 worth of DAI as collateral
   2. $8,000 worth of USDC as debt
2. A liquidator decides to cover $4,500 of the user’s USDC debt. The parameters for the liquidationCall would be:
   1. collateralAsset: DAI (the collateral asset).
   2. debtAsset: USDC (the debt to be repaid).
   3. user: The address of the borrower.
   4. debtToCover: $4,500 (amount of USDC debt being repaid).
   5. receiveAToken: false (liquidator wants to receive DAI directly instead of myTokens).
3. The liquidator signs and sends the transaction. Upon successful execution, the liquidator repays $4,500 of the user’s USDC debt and receives an equivalent amount of DAI (plus the liquidation incentive).

#### **Use Cases for Liquidation:**

* Risk Management: Liquidation allows the protocol to recover assets from accounts that are at risk of default, thus ensuring the security of the protocol’s reserves.
* Market Stability: Liquidations ensure that bad debts do not accumulate, maintaining overall market stability.
* Incentives for Liquidators: The Liquidation Incentive motivates participants to take part in the process, further ensuring that unhealthy positions are addressed swiftly.

For more technical details on liquidations, refer to Aave's liquidation documentation:

* Prerequisites:[ Aave Docs - Prerequisites](https://aave.com/docs/developers/liquidations#prerequisites)
* Finding accounts to liquidate:[ Aave Docs - Getting Accounts](https://aave.com/docs/developers/liquidations#getting-accounts-to-liquidate)
* Executing liquidation:[ Aave Docs - Executing Liquidation](https://aave.com/docs/developers/liquidations#executing-the-liquidation-call)

#### Rates and Incentives

The interest rate strategy and incentives for Mystic Finance will be designed to incentivize lending and borrowing, potentially including Mystic tokens as rewards. As for interest rate strategies, refer to the Aave documentation:

* [Interest Rate Strategy](https://aave.com/docs/developers/smart-contracts/interest-rate-strategy)

​​

### KYC (Know Your Customer)

Some pools on Mystic require participants to KYC.  This process ensures that both Mystic and all its users adhere to the relevant regulatory standards before interacting with the system.

### **KYC Process Flow**

1. User Initiates KYC Process:
   1. To access the Borrow and Repay functionalities *in certain pools*, users must first complete the KYC verification process. This can be done through the Mystic Finance UI by navigating to the KYC section.
2. Personal Data Submission: to complete KYC, users must submit:
   1. Full Name: As per your official documents.
   2. Address: Your residential address.
   3. Proof of Identity: Government-issued ID (passport, driver’s license, etc.).8Proof of Address: Utility bills, bank statements, etc.

Here's an example KYC data flow:\
\
{

&#x20;  "fullName": "John Doe",

&#x20;  "address": "123 Main Street",

&#x20;  "idType": "passport",

&#x20;  "idNumber": "AB1234567",

&#x20;  "proofOfAddress": "utility\_bill.pdf",

&#x20;  “walletAddress”:”0x1”

}

3. KYC Approval:
   1. After submission, the submission is verified. If approved, users will receive a notification (either via email or directly on the platform) indicating that the KYC process has been successfully completed and their account has been approved for borrowing and repaying.

Example KYC notification status:\
\
{

&#x20;  "kycStatus": "approved",

&#x20;  "timestamp": "2024-10-20T12:00:00Z",

&#x20;  "userID": "0x1"

}

4. Borrowing and Repayment Access:
   1. Once the KYC is approved, users will get their Mystic ID minted as a Soulbound token and be able to access new roles for borrowing and repaying. This verification step ensures that only eligible users may access specific financial instruments.

**KYC Data Handling**

* Security and Privacy: All personal data submitted during the KYC process is handled securely and in compliance with GDPR and other privacy laws. Mystic Finance follows industry best practices to ensure user data is protected.

### Custody of Assets

Mystic Finance ensures that user assets are securely stored with a third-party custodian, in compliance with regulatory requirements (i.e. EU financial regulations). This allows the platform to operate legally while maintaining the highest standards of security for user assets.

#### **Custody Overview**

1. Proposed Custodian: Mystic Finance collaborates with a regulated custodian for the safekeeping of assets. This custodian is responsible for ensuring the security of user assets and compliance with applicable regulations. The custodian is chosen based on their adherence to regulatory standards, security protocols, and overall trustworthiness. Users will be proposed such a custodian via the Mystic UI.
2. Regulatory Compliance: As a financial platform, Mystic Finance complies with EU regulations and other international financial laws regarding the storage and security of user assets. This includes requirements around asset segregation, reporting, and regular audits of the custodian’s security protocols.
3. Regulatory guidelines:
   1. MiCA (Markets in Crypto-Assets Regulation): Ensures compliance with EU financial regulations for digital assets.
   2. GDPR (General Data Protection Regulation): Ensures that all user data is handled with the highest level of privacy and security.
4. Asset Security:
   1. The custodian takes care of the safekeeping of assets by using advanced security mechanisms such as multi-signature wallets, cold storage, and encryption to protect user funds from hacking or loss.
   2. Users can rest assured that their assets are protected without needing to take any direct action for the security or custody process.
5. No User Interaction Required:
   1. The custody process is fully automated and transparent to the end user. Once assets are deposited or borrowed within the protocol, they are automatically transferred to the custodian’s infrastructure, where they are securely held.
   2. There is no manual intervention required from the user in terms of custody management.

#### **Custody Example Flow**

After a user deposits assets on the Mystic Finance platform, the assets are transferred to the custodian’s secure storage and can be transferred back to the pool later when needed. Do note, users can still interact with the protocol (e.g. borrowing or repaying), but the assets themselves remain securely held by the custodian.

#### **Custody Benefits**

* Compliance: The custody model ensures the platform complies with regulations that require assets to be stored securely by a regulated third party.
* Security: Multi-layer security protocols are in place to ensure that assets are protected from malicious attacks or internal failures.

No User Responsibility: Users do not need to take any action or worry about managing the custody of their assets. This responsibility is entirely managed by the custodian.


# Liquidity Optimization Strategies

In order to further adjust to the needs of RWAs and solve for the constant problem of liquidity fragmentation, Mystic employs a series of liquidity optimization strategies that benefit all users and make the platform further unique to the RWA use case. Lets look at what they are.


# Leveraged Borrowing

Mystic Finance provides users with two leverage borrowing strategies, enabling them to amplify their investment returns. Two key strategies are employed: flash loans and looping.

### **Flash Loans for Leverage**

Flash loans are a powerful DeFi tool that allows users to borrow large amounts of capital without providing upfront collateral, as long as the borrowed amount is returned within the same transaction block. Mystic Finance leverages flash loans to enable users to perform a complex financial operation of increased collateral positions by flash borrowing and collateral swapping without exposure to significant risk.

For example, a user might use a flash loan to temporarily acquire additional RWA collateral, which is then used to secure a larger loan. The original flash loan is repaid within the same transaction, often generating a profitable position without requiring the user to risk their own capital. This strategy will be readily available to all users on Mystic Finance.

### **Looping for Leverage**

Looping is another leverage strategy where users borrow against their collateral, and then use the borrowed funds to purchase more of the same collateral, which is then deposited back into the platform to borrow even more. This process can be repeated multiple times, effectively leveraging the user’s initial collateral into a much larger position.

While looping can significantly amplify returns, it also increases risk, as the user’s position becomes more sensitive to price fluctuations in the underlying asset. Mystic Finance provides tools and safeguards against this, such as automated monitoring and adjustable leverage limits, to help users manage these risks effectively. However, it is important to reiterate the more complex and riskier nature of these operations and recommend that only knowledgeable participants engage in them.


# RWA Exposure via USDC Deposits

Mystic Finance offers a unique solution for users who want exposure to RWAs without directly holding them. By depositing USDC, users can leverage the platform’s smart contracts to purchase RWAs on their behalf. These RWAs are then used as collateral, allowing users to borrow against the purchased RWAs without ever owning them.

### **Seamless Integration**

This strategy is designed to be user-friendly, eliminating the need for users to interact directly with the complexities of RWA management or having to KYC. The platform handles all aspects of the asset purchase, collateralization, and redemption processes, providing a seamless experience that minimizes risk and maximizes convenience.

### **Flexibility and Control**

Users maintain full control over their investments, with the ability to withdraw or redeem their USDC funds at any time. This strategy provides the benefits of RWA exposure, such as stable returns and portfolio diversification, without the challenges of asset management or KYC processes.


# Hybrid Pools

Mystic Finance offers users the opportunity to supply liquidity to various pools made up of only RWA assets, only digital assets or a mixture of both. This strategy is designed to manage risk and optimize returns by allowing users to invest in a mix of pools with varying risk profiles and return potentials.

### **Risk Segmentation**

The baskets are segmented based on different risk categories, ranging from low-risk, bluechip assets such as treasury bills or stETH, to higher-risk, higher-reward assets like stocks or LSTs. Users can choose to allocate their capital across the different pools in a basket, balancing their portfolios according to their risk tolerance and investment goals.

### **ROI Optimization**

By investing in a basket of RWA pools, users benefit from the diversification of their portfolio, which can enhance returns while mitigating the impact of poor performance in any single pool. Mystic Finance’s algorithm dynamically adjusts the weightings within each basket based on market conditions, ensuring market health and consistent returns.

### **Dynamic Allocation**&#x20;

Mystic Finance continuously monitors the performance of its RWA pools and digital asset markets to adjust liquidity allocation dynamically. This ensures that capital is always directed towards the most profitable and stable opportunities.

### **Customizable Baskets**

For advanced users, Mystic Finance offers customizable pool baskets, where lenders can manually select and adjust the composition of their investments. This feature is particularly useful for institutional investors or experienced traders who wish to tailor their exposure to specific assets.


# Audits

Here are listed Mystic's official audit reports from independent security companies

| Scope                                               | Auditor                                                                    | Report                                                                                                                                                                                          |
| --------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Morpho Contracts                                    | [Auditors here](https://docs.morpho.org/getting-started/resources/audits/) | [Audits here](https://docs.morpho.org/getting-started/resources/audits/)                                                                                                                        |
| Permissioning and custodian integration (sunsetted) | [Hacken](https://hacken.io/)                                               | [Audit here](https://hacken.io/audits/mystic-finance)                                                                                                                                           |
| Liquid Staking Token (myPLUME)                      | [Kann Audits](https://kannaudits.com/)                                     | [Audit 1 here](https://github.com/Kann-Audits/Kann-Audits/blob/main/reports/pdf-format/Mystic%20Finance.pdf)                                                                                    |
| Liquid Staking Token (myPLUME)                      | [Kann Audits](https://kannaudits.com/)                                     | [Audit 2 here](https://github.com/Kann-Audits/Kann-Audits/blob/main/reports/pdf-format/Mystic%20Finance_v2.pdf)                                                                                 |
| Liquid Staking Token (myPLUME)                      | [Octane Security](https://www.octane.security/)                            | [Audit here](https://drive.google.com/file/d/1_rKbNL9EFs3KT6x_amWQTyrzlUTvt0R5/view?usp=sharing) and [here](https://drive.google.com/file/d/1WZ7LJIszm-GOsrHX5-MhIhDLADwSXmzh/view?usp=sharing) |
| Liquid Staking Token (myPLUME)                      | [Kann Audits](https://kannaudits.com/)                                     | [Audit here](https://drive.google.com/file/d/1O8Er8ZmVqqSmHuYteDkaBOhh7lBG0uQs/view?usp=sharing)                                                                                                |
| RFQ API                                             | [Kann Audits](https://kannaudits.com/)                                     | [Audit here](https://drive.google.com/file/d/1LerNUP9OjGmfDgyTsr11DxtGMJeCP3dR/view?usp=sharing)                                                                                                |


# Media Kit

## Logo

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FtzHWKu2k11yLBXXfU4xI%2Fmystic_transparent_black%20(3).png?alt=media&amp;token=f00a8aec-2309-47c6-abec-c50d9d0fe797" alt="" width="188"><figcaption><p>Mystic Logo - black</p></figcaption></figure>

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2F1Z1ocDdFUe0uCuoRuXRl%2Fmystic_transparent_white%20(2)%20(1).png?alt=media&amp;token=65c400a6-c199-4c31-a4e6-f75ac3505356" alt="" width="188"><figcaption><p>Mystic logo - white</p></figcaption></figure>

## Logo mark

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FBdepnYzSfS5yONrWYZn6%2Ffaviconasquare.png?alt=media&amp;token=cfb03798-5526-4e37-898f-29f4c68f9d74" alt="" width="78"><figcaption><p>Mystic diamond</p></figcaption></figure>

<div align="center"><figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FWv6S3aJPneFqInWrE6fS%2Fmystic_m_transparent_black%20(1).png?alt=media&amp;token=5c5870d2-6c16-455c-93f5-4b9f478e53f0" alt="" width="188"><figcaption><p>Logo mark - black</p></figcaption></figure></div>

<figure><img src="https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FXE54S2XHGH4phEq80OFl%2Fmystic_m_transparent_white%20(2).png?alt=media&amp;token=5babcdd5-40d1-4912-bb28-bf66fb7eb883" alt="" width="188"><figcaption><p>Logo mark - white</p></figcaption></figure>

Download:

[MysticFinanceLogo.zip](broken://pages/af2pyefthGLv0UkHPBYp)

## Brand colors

![background.png](https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FXXEkNVoKKRS9PYWCXND8%2Fbackground.png?alt=media)

&#x20;\#131421

![foreground.png](https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FG7GLJvouMV77HriVccuz%2Fforeground.png?alt=media)

&#x20;\#120E1A

![accent1.png](https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FZ9a7AjxrfjXAoxaIQkxn%2Faccent1.png?alt=media)

&#x20;\#FF7B00

![accent2.png](https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FHmGQEWnkpzJmobE3xEJp%2Faccent2.png?alt=media)

&#x20;\#8139B8

## Typography

**Headings**

![IMB\_Plex\_Sans.png](https://992298956-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6tbTSDm2OHcYh0HPXs3Q%2Fuploads%2FYaSMQi9I6oKiaQQd7E8O%2FIMB_Plex_Sans.png?alt=media)

[Get IMB Plex Sans](https://fonts.google.com/specimen/IBM+Plex+Sans)


# Terms of Use

Mystic Finance, operated by Mystic Protocol Unipessoal Lda and based in Lisboa Portugal,  adheres to a strict set of terms and conditions designed to protect users and ensure the integrity of the platform. Key points include:

* Eligibility: Users must comply with all applicable laws and regulations in their jurisdiction. Mystic Finance reserves the right to restrict access to the platform based on geographic location, age, or other criteria.
* Risk Disclosure: Users acknowledge that participating in DeFi platforms carries inherent risks, including but not limited to market volatility, smart contract vulnerabilities, and regulatory changes. Mystic Finance provides no guarantees of returns or protection against losses.
* User Responsibilities: Users are responsible for securing their accounts, including the safekeeping of private keys and other authentication methods. Mystic Finance is not liable for any loss or theft resulting from user negligence.
* Transaction Fees: Mystic Finance charges transaction fees for various services, including swaps, redemptions, and borrowing. These fees are transparently communicated to users prior to the execution of any transaction.
* Dispute Resolution: Any disputes arising from the use of Mystic Finance will be resolved through arbitration in accordance with the laws of the platform's jurisdiction.

For more detailed information on the applicable terms of Mystic, please see our [Terms of Use](https://www.mysticfinance.xyz/terms-of-service).


# Privacy Policy

Mystic Protocol Unipessoal Lda, with headquarters in Rua General Morais Sarmento nº7 2ºEsquerdo, 1500-310 Lisboa, Portugal, operates the website "Mystic Finance" at <https://mysticfinance.xyz>. We take your privacy seriously. To better protect your privacy, we provide this privacy policy notice explaining the way your personal information is collected and used. For any queries, please contact us at <joao.moreira@mysticfinance.xyz>.\
\
**Collection of Routine Information**\
\
This website track basic information about its visitors. This information includes, but is not limited to, IP addresses, browser details, timestamps and referring pages. None of this information can personally identify specific visitors to this website. The information is tracked for routine administration and maintenance purposes.\
\
**Cookies**\
\
Where necessary, this website uses cookies to store information about a visitor’s preferences and history to better serve the visitor and/or present the visitor with customized content.\
\
**Advertisement and Other Third Parties**\
\
Advertising partners and other third parties may use cookies, scripts and/or web beacons to track visitor activities on this website to display advertisements and other useful information. Such tracking is done directly by the third parties through their servers and is subject to their privacy policies. This website has no access or control over these cookies, scripts and/or web beacons that may be used by third parties. We have included links on this website for your use and reference. We are not responsible for the privacy policies on these websites. You should be aware that the privacy policies of these websites may differ from our own. Link to the privacy policy of third-party service providers used by the website: [Google Analytics](https://policies.google.com/privacy).\
\
**Security**\
\
The security of your personal information is important to us, but remember that no method of transmission over the Internet, or method of electronic storage, is 100% secure. While we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security.\
\
**Changes To This Privacy Policy**\
\
This Privacy Policy is effective as of 2024-04-10 and will remain in effect except concerning any changes in its provisions in the future, which will be in effect immediately after being posted on this page. We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. If we make any material changes to this Privacy Policy, we will notify you either through the email address you have provided us or by placing a prominent notice on our website.\
\
**Contact Information**\
**‍**\
For any questions or concerns regarding the privacy policy, please send us an email at <joao.moreira@mysticfinance.xyz>.


