> For the complete documentation index, see [llms.txt](https://ffp.gitbook.io/fusionfi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ffp.gitbook.io/fusionfi/fusionfi-protocol-cn/kai-fa-zhi-nan/blueprints/basic-lan-tu/wu-ben-tao-li-agent.md).

# 无本套利 Agent

基于 Basic 蓝图实现 FFP 协议中的无本套利 Agent。该 Agent 能够在无需资金成本的情况下，基于 FFP 协议中的价差自动执行套利操作。

以下以 wAR/wUSDC 交易对为例，介绍无损套利 Agent 的实现过程。

### **目标场景**

1. 监听 FFP 协议中 wAR/wUSDC 交易对的兑换订单：
   1. Agent 实时从 FFP 结算中心获取等待执行的 wAR/wUSDC 交易对的挂单。
   2. 把每个挂单价格和 FFP 中的 wAR/wUSDC AMM Agent 对比价差。
2. 当两者存在价格差时执行套利：
   1. 假设发现某个用户挂单为 1 wAR 兑换 20 wUSDC。
   2. wAR/wUSDC AMM Agent 此时价格为 1 wAR 兑换 21 wUSDC。
   3. 套利操作为：
      1. 请求 AMM Agent 创建一个 21 wUSDC 兑换 1wAR 的挂单到 FFP。
      2. 套利 Agent 把用户的挂单给 AMM Agent 创建的挂单提交给 FFP 结算中心。
      3. 结算中心的结算逻辑是 收到用户的 1 wAR 和 AMM Agent 发出的 21 wUSDC, 然后转出 20 wUSDC 给用户，1 wAR 给 AMM Agent，最终结余 1 wUSDC。
      4. 结算中心会把结余的 1 wUSDC 发送给套利 Agent，因为这笔订单的结算者为套利 Agent。

### **套利 Agent 代码解析**

<details>

<summary>arbitrage.lua</summary>

```lua
local json = require('json')
local utils = require('.utils')
local bint = require('.bint')(512)

ProcessCfg = {
    PageSize = '10',

    PoolPid = 'vJY-ed1Aoa0pGgQ30BcpO9ehGBu1PfNHUlwV9W8_n5A', -- wUSDC/wAR
    wUSDCPid = '7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ', -- Y
    wARPid = 'xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10' -- X
}

--[[
example-notes:
    [
        {
            "Price": 0.000023,
            "IssueDate": 1732447831435,
            "Status": "Open",
            "ID": 2303,
            "HolderAssetID": "7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ",
            "AssetID": "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10",
            "Amount": "50000000000",
            "Issuer": "ZsXb0JKGATswuk-qRs3iV49VbTbaiaMCEPYqk6GKdW0",
            "HolderAmount": "1150000",
            "NoteID": "ssfaQ3p3Ufj1rcKj1ZXVW3aYYW2zhcfAQ8vvIkSOsD4"
        }
    ]
-- ]]
function getNotesFromSettle(assetID, holderAssetID, page)
    print('getNotesFromSettle...'..assetID)
    local res = Send({
        Target = FFP.Settle,
        Action = 'GetNotes',
        AssetID = assetID,
        HolderAssetID = holderAssetID,
        Order = 'desc',
        Status = 'Open',
        Page = tostring(page),
        PageSize = ProcessCfg.PageSize
    }).receive()
    local notes = json.decode(res.Data)
    print('got notes: '.. #notes)
    return notes
end

function getPoolInfo()
    print('getPoolInfo: ' .. ProcessCfg.PoolPid)
    local res = Send({
        Target = ProcessCfg.PoolPid,
        Action = 'Info',
    }).receive()
    if not res then
        print('Error calling Send: ')
        return {}
    end
    print('got poolInfo')
    return {
        ["Executing"] = res.Tags.Executing,
        ["PX"] = res.Tags.PX,
        ["PY"] = res.Tags.PY,
        ["X"] = res.Tags.X,
        ["Y"] = res.Tags.Y,
        ["Fee"] = res.Tags.Fee
    }
end

-- return amountOut
local function calcAmmInputPrice(amountIn, reserveIn, reserveOut, Fee)
    local amountInWithFee = bint.__mul(amountIn, bint.__sub(10000, Fee))
    local numerator = bint.__mul(amountInWithFee, reserveOut)
    local denominator = bint.__add(bint.__mul(10000, reserveIn), amountInWithFee)
    return bint.udiv(numerator, denominator)
end

-- makeOrder for amm pool
local function makeOrderForAmmPool(tokenIn, amountIn, tokenOut, amountOut)
    print('makeOrderForAmmPool...')
    local res = Send({
        Target = ProcessCfg.PoolPid,
        Action = 'MakeOrder',
        TokenIn = tokenIn,
        AmountIn = amountIn,
        TokenOut = tokenOut,
        AmountOut = amountOut
    }).receive()
    if res.Error then
        return {
            ["Error"] = res.Error
        }
    end
    local note = json.decode(res.Data)
    return {
        ['Note'] = note
    }
end

local function takeOrder(noteIDs)
    -- start Settle
    local res = Send({
        Target = ao.id,
        Action = 'FFP.TakeOrder',
        Data = json.encode(noteIDs)
    }).receive()
    if res.Error then
        return {
            ["Error"] = res.Error
        }
    end
    return {}
end

-- arbitrageExecutor
function perNoteArbitrageExecutor(note)
    local tokenIn = note.AssetID
    local amountIn = note.Amount
    local tokenOut = note.HolderAssetID
    local expectedAmountOut = note.HolderAmount

    local poolInfo = getPoolInfo()
    if poolInfo.Executing == 'true' then
        print('ammPool is executing...')
        return {
            ["Error"] = 'err_pool_executing'
        }
    end

    local reserveIn = poolInfo.PX
    local reserveOut = poolInfo.PY
    if tokenIn == poolInfo.Y then
        reserveIn, reserveOut = reserveOut, reserveIn
    end

    local amountOut = calcAmmInputPrice(bint(amountIn), bint(reserveIn), bint(reserveOut), bint(poolInfo.Fee))
    if utils.lt(amountOut, expectedAmountOut) then
        print('amount out too small')
        return {
            ['Error'] = 'amm_prices_low'
        } -- can not settle, because pool price not ok
    end

    -- could process settle
    -- makeOrder for amm pool
    local ord = makeOrderForAmmPool(tokenIn, amountIn, tokenOut, tostring(amountOut))
    if ord.Error then
        print('makeOrderForAmmPool failed: ' .. ord.Error)
        return {
            ['Error'] = ord.Error
        }
    end

    print('amm makeorder note: '.. ord.Note.NoteID)
    -- calc settle income and outcome 
    local si, so = utils.SettlerIO({ord.Note, note})
 
    if next(so) then
        print('some calc error, settlerout must be nil')
        return {
            ["Error"] = 'err_settler_out'
        }
    end
    -- take order use basic api 
    print('Taking order for NoteIDs: ' .. json.encode({ord.Note.NoteID, note.NoteID}))
    local res = takeOrder({ord.Note.NoteID, note.NoteID})
    if res.Error then
        print('take order failed: ' .. res.Error)
        return {
            ['Error'] = res.Error
        }
    end
    return {
        ['SettlerIn'] = si
    }
end

function arbitrageExec(assetID, holderAssetID)
    local page = 1
    while true do
        -- get wUSDC swap wAR notes
        local notes = getNotesFromSettle(assetID, holderAssetID, page)
        if #notes == 0 then
            print('not notes...')
            return
        end

        for _, note in ipairs(notes) do
            -- execute per note
            local res = perNoteArbitrageExecutor(note)
            if res.Error then
                print('perNoteArbitrageExecutor failed: '..res.Error..' noteID: '.. note.NoteID)
            else
                print('success arbitrage note: '.. note.NoteID .. ' settlerIn: ' .. json.encode(res.SettlerIn))
            end
        end

        if #notes < tonumber(ProcessCfg.PageSize) then
            break
        end
        page = page + 1
    end
end

function ArbitrageExecutor()
    print('start wAR-wUSDC notes arbitrage...')
    arbitrageExec(ProcessCfg.wARPid,ProcessCfg.wUSDCPid)
    print('end wAR-wUSDC pairs arbitrage...')
    print('start wUSDC-wAR notes arbitrage...')
    arbitrageExec(ProcessCfg.wUSDCPid,ProcessCfg.wARPid)
    print('end wUSDC-wAR notes arbitrage...')
end




return {
    ArbitrageExecutor = ArbitrageExecutor
}

```

</details>

<details>

<summary>agent.lua</summary>

```lua
local json = require('json')
local bint = require('.bint')(512)
local utils = require('.utils')
local agent = require('.arbAgent')

FFP = FFP or {}
-- config
FFP.Settle = FFP.Settle or 'rKpOUxssKxgfXQOpaCq22npHno6oRw66L3kZeoo_Ndk'
FFP.Version = FFP.Version or '0.31'
FFP.MaxNotesToSettle = FFP.MaxNotesToSettle or 2

-- database
FFP.Notes = FFP.Notes or {}
FFP.Settled = FFP.Settled or {}

Handlers.add('ffp.withdraw', 'FFP.Withdraw',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can withdraw')
    assert(type(msg.Token) == 'string', 'Token is required')
    assert(type(msg.Amount) == 'string', 'Amount is required')
    assert(bint.__lt(0, bint(msg.Amount)), 'Amount must be greater than 0')
    
    Send({ 
      Target = msg.Token, 
      Action = 'Transfer', 
      Quantity = msg.Amount, 
      Recipient = Owner
    })
  end
)

Handlers.add('ffp.takeOrder', 'FFP.TakeOrder',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can make order')
    
    local noteIDs = utils.getNoteIDs(msg.Data)
    if noteIDs == nil then
      msg.reply({Error = 'err_invalid_note_ids', ['X-FFP-TakeOrderID'] = msg.Id})
      return
    end
    
    if #noteIDs > FFP.MaxNotesToSettle then
      msg.reply({Error = 'err_too_many_orders', ['X-FFP-TakeOrderID'] = msg.Id})
      return
    end

    local notes = {}
    for _, noteID in ipairs(noteIDs) do
      local data = Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data
      if data == '' then
        msg.reply({Error = 'err_not_found', ['X-FFP-TakeOrderID'] = msg.Id})
        return
      end
      local note = json.decode(data)
      if note.Status ~= 'Open' then
        msg.reply({Error = 'err_not_open', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
        return
      end
      if note.Issuer == ao.id then
        msg.reply({Error = 'err_cannot_take_self_order', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
        return
      end
      if note.ExpireDate and note.ExpireDate < msg.Timestamp then
        msg.reply({Error = 'err_expired', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
        return
      end
      table.insert(notes, note)
    end
    
    local si, so = utils.SettlerIO(notes)
    -- todo: make sure we have enough balance

    -- start a settle session
    local res = Send({Target = FFP.Settle, Action = 'StartSettle', Version = FFP.Version, Data = json.encode(noteIDs)}).receive()
    if res.Error then
      msg.reply({Error = res.Error, ['X-FFP-TakeOrderID'] = msg.Id})     
      return
    end
   
    local settleID = res.SettleID
    FFP.Settled[settleID] = {SettleID = settleID, NoteIDs = noteIDs, Status = 'Pending'}
   
    if next(so) == nil then
      print('TakeOrder: Settler no need to transfer to settle process')
      Send({Target = FFP.Settle, Action = 'Settle', Version = FFP.Version, SettleID = settleID})
      msg.reply({SettleID = settleID})
      return
    end

    for k, v in pairs(so) do
      local amount = utils.subtract('0', v)
      Send({Target = k, Action = 'Transfer', Quantity = amount,  Recipient = FFP.Settle, 
        ['X-FFP-SettleID'] = settleID, 
        ['X-FFP-For'] = 'Settle'
      })
    end
    msg.reply({SettleID = settleID})
  end
)

Handlers.add('ffp.done',
  function(msg) 
    return (msg.Action == 'Credit-Notice') and (msg['X-FFP-For'] == 'Settled' or msg['X-FFP-For'] == 'Refund') 
  end,
  function(msg)
    if msg.Sender ~= FFP.Settle then
      print('Only settle can send settled notice')
      return
    end

    local settleID = msg['X-FFP-SettleID']
    if settleID and FFP.Settled[settleID] then
      if msg['X-FFP-For'] == 'Settled' then
        FFP.Settled[settleID].Status = 'Settled'
      else
        FFP.Settled[settleID].Status = 'Rejected'
      end
      FFP.Settled[settleID].SettledDate = msg['X-FFP-SettledDate']
      for _, noteID in ipairs(FFP.Settled[settleID].NoteIDs) do
        local data = Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data
        FFP.Notes[noteID] = json.decode(data)
      end
    else
      print('SettleID not found: ' .. settleID)
    end
  end
)

Handlers.add('ffp.getOrders', 'FFP.GetOrders',
  function (msg)
    msg.reply({Action = 'GetOrders-Notice', Data = json.encode(FFP.Notes)})
  end
)

Handlers.add('ffp.getSettled', 'FFP.GetSettled',
  function (msg)
    msg.reply({Action = 'GetSettled-Notice', Data = json.encode(FFP.Settled)})
  end
)

-- Trigger
ExecuteState = ExecuteState or 'ON'
Handlers.add(
    "CronTick",
    Handlers.utils.hasMatchingTag("Action", "Cron"), -- Send({Target=ao.id,Action='Cron'})
    function()
        print('Cron trigger arbitrage ...')
        if ExecuteState == 'ON' then
            -- find note: wUSDC swap wAR
            ExecuteState = 'OFF'
            agent.ArbitrageExecutor()
            ExecuteState = 'ON'
            print('finished..')
            agent.Withdrawal()
            print('finish withdrawal...')
        else
          print('ExecuteState is off...')
        end
    end
)
```

</details>

<details>

<summary>utils.lua</summary>

```lua
local json = require('json')
local bint = require('.bint')(512)

local mod = {
  add = function(a, b)
      return tostring(bint(a) + bint(b))
  end,
  subtract = function(a, b)
      return tostring(bint(a) - bint(b))
  end,
  toBalanceValue = function(a)
      return tostring(bint(a))
  end,
  toNumber = function(a)
      return tonumber(a)
  end,
  lt = function (a, b)
      return bint.__lt(bint(a), bint(b))
  end
}

mod.getNoteIDs = function(data)
  if string.find(data, "null") then
    return nil
  end
  
  local noteIDs, err = json.decode(data)
  if err then
    return nil
  end

  if type(noteIDs) == "string" then
    return {noteIDs}
  end

  if type(noteIDs) == "table" then
    for i = 1, #noteIDs do
      if noteIDs[i] == nil or type(noteIDs[i]) ~= "string" then
        return nil
      end
    end
    return noteIDs
  end

  return nil
end

mod.SettlerIO = function (notes)
  local settlerIn = {}
  local settlerOut = {}

  local bc = {}
  for _, note in ipairs(notes) do
      if not bc[note.AssetID] then bc[note.AssetID] = bint(0) end
      if not bc[note.HolderAssetID] then bc[note.HolderAssetID] = bint(0) end
      bc[note.AssetID] = mod.add(bc[note.AssetID], note.Amount)
      bc[note.HolderAssetID] = mod.subtract(bc[note.HolderAssetID], note.HolderAmount)
  end
  
  for k, v in pairs(bc) do
      if v == '0' then bc[k] = nil end
  end

  for k, v in pairs(bc) do
      if mod.lt(v, '0') then
          settlerOut[k] = v
      else
          settlerIn[k] = v
      end
  end

  return settlerIn, settlerOut
end
    
return mod
```

</details>

#### 代码结构

* arbitrage.lua 实现套利的所有执行逻辑。
* agent.lua 在 basic.lua 代码基础上添加了套利相关的方法。
* utils.lua 是 basic 蓝图复制过来的。

#### 核心方法解读

#### 1. ArbitrageExecutor

套利总调度器，调度整个套利过程：

* 首先处理 wAR -> wUSDC 的挂单套利。
* 然后处理 wUSDC -> wAR 的挂单套利。
* 这是为了覆盖两种可能得套利方向。

#### 2. arbitrageExec

用户执行某个交易方向的所有挂单的套利操作：

* 通过传入的两个资产类型参数，确定需要执行套利方向。
* 从 FFP 中分页获取挂单（Notes）数据。
* 对每个 Note 执行套利操作。

#### 3. getNotesFromSettle

专门用于从 FFP 中获取挂单函数：

* 通过资产类型和 Note 状态筛选需要获取的 Notes 从 Settle Process 中获取

#### 4. perNoteArbitrageExecutor

处理单个 Note 的套利逻辑：

* 从 AMM Agent 获取流动性信息，确保池状态可用（未锁定）。
* 根据池流动性信息计算兑换价格：
  * 如果 AMM 的价格小于 Note 的目标价格，则退出套利（价格不匹配）
* 请求 AMM 挂单：
  * 如果 AMM 的价格大于 Note 的目标价格，会请求 AMM Agent 按照最好的价格发起一个 Note 对手单 Note，这两个 Note 的价差就是套利收益。
  * 比如 Note 是 1 wAR 兑换 20 wUSDC，如果 AMM 此时的价格可以发起 Note 为 21  wUSDC 兑换 1 wAR ，这两个 Note 结算之后会有 1 wUSDC 的价差。
* 套利结算：
  * 调用 takeOrder 发起结算，完成套利操作。
  * takeOrder 是 basic 蓝图实现的方法，套利 Agent 可以直接调用。

等待结算中心完成结算之后，Agent 就自动收取到两个 Note 的价差收益。

#### 5. calcAmmInputPrice

该方法基于恒定乘积公式（UniswapV2 算法）和手续费计算 AMM 的兑换价格。在 perNoteArbitrageExecutor 方法中，首先请求 AMM 池信息，获取 reserveIn、reserveOut 和 Fee 参数。随后，从挂单（Note）中提取输入金额 AmountIn，并计算 AMM 当前可兑换的 AmountOut。接着，将 AmountOut 与挂单中的期望兑换金额进行对比。如果 AmountOut 大于期望值，则存在套利空间，继续执行套利操作。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ffp.gitbook.io/fusionfi/fusionfi-protocol-cn/kai-fa-zhi-nan/blueprints/basic-lan-tu/wu-ben-tao-li-agent.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
