{
  "openapi": "3.1.0",
  "info": {
    "title": "Async Swap API",
    "version": "0.0.2",
    "description": "Async Swap is a limit-order protocol on Uniswap v4. Makers post\norders on-chain; fillers settle them on-chain at\nthe maker's minimum output plus the deployment's protocol fee.\nThe order API is an indexer view: verify current on-chain state before preparing a fill.\nFiller REST data covers the deployed Unichain and Arc mainnets and the four indexed testnets. A confirmed order from the Async Swap app can appear in the open book before the durable indexer reaches its block.\nTestnet contract addresses and indexer start blocks: https://github.com/asyncswap/contracts/blob/main/docs/deployments.md.\nSalt-based orders and legacy nonce-based orders may coexist. Use the Router and ABI for the order's original hook.\nThe current source requires asset/USDC pools; this is not a guarantee about older deployments.\nFiller preparation and Unichain bundle construction are local CLI operations, not REST submission endpoints.\n"
  },
  "servers": [
    {
      "url": "https://api.asyncswap.org",
      "description": "Production"
    },
    {
      "url": "http://localhost:42070",
      "description": "Local api (or SSH tunnel to prod)"
    },
    {
      "url": "http://localhost:42069",
      "description": "Local test server"
    }
  ],
  "tags": [
    {
      "name": "Filler",
      "description": "Read open orders, fetch lifecycle events, and look up pool metadata."
    },
    {
      "name": "Streaming",
      "description": "Live order updates over WebSocket.\n\n### Endpoint\n\n`wss://api.asyncswap.org/v1/filler/stream`\n\n### Protocol\n\nThe server broadcasts every chain's order changes to each connection. Filter by `chainId` in the client. Each frame carries one change:\n\n```json\n{ \"type\": \"order.created\", \"chainId\": 130, \"orderId\": \"0x...\", \"poolId\": \"0x...\", \"txHash\": \"0x...\", \"blockNumber\": 12345 }\n{ \"type\": \"order.updated\", \"chainId\": 130, \"orderId\": \"0x...\", \"poolId\": \"0x...\", \"txHash\": \"0x...\", \"blockNumber\": 12346 }\n```\n\n- Fetch `/v1/filler/orders?chainId=...&status=open` after a matching frame for the current book.\n- Indexer frames include `order.priceUpdate`, `order.filled`, `order.cancelled`, and `order.matched`.\n- Unknown `type` values must be ignored — new event kinds may be\n  added without a breaking version bump.\n\n### Browser\n\n```js\nconst ws = new WebSocket(\"wss://api.asyncswap.org/v1/filler/stream\");\n\nws.addEventListener(\"message\", (e) => {\n  const msg = JSON.parse(e.data);\n  if (msg.chainId !== 130) return;\n  switch (msg.type) {\n    case \"order.created\":     /* new ask appeared      */ break;\n    case \"order.priceUpdate\": /* maker repriced        */ break;\n    case \"order.filled\":      /* someone filled it     */ break;\n    case \"order.cancelled\":   /* maker withdrew        */ break;\n  }\n});\n```\n\n### Node.js\n\nSame shape as the browser snippet using the [`ws`](https://www.npmjs.com/package/ws) package:\n\n```js\nimport WebSocket from \"ws\";\n\nconst ws = new WebSocket(\"wss://api.asyncswap.org/v1/filler/stream\");\n\nws.on(\"message\", (raw) => {\n  const msg = JSON.parse(raw.toString());\n  if (msg.chainId === 130) console.log(msg.type, msg.orderId);\n});\n```\n\n### Python\n\nWith [`websockets`](https://websockets.readthedocs.io/):\n\n```python\nimport asyncio, json, websockets\n\nasync def main():\n    url = \"wss://api.asyncswap.org/v1/filler/stream\"\n    async with websockets.connect(url) as ws:\n        async for raw in ws:\n            msg = json.loads(raw)\n            if msg.get(\"chainId\") == 130: print(msg[\"type\"], msg.get(\"orderId\"))\n\nasyncio.run(main())\n```\n\n### Quick test (CLI)\n\nSmoke-test the endpoint with [`wscat`](https://github.com/websockets/wscat):\n\n```bash\nwscat -c wss://api.asyncswap.org/v1/filler/stream\n```\n\n### Reconnect\n\nTreat the connection as eventually unreliable: idle proxies, mobile\nnetwork changes, and server deploys can close it without warning.\nReconnect with exponential backoff and refresh the order list\non every reopen, since events sent while disconnected are not replayed."
    }
  ],
  "paths": {
    "/v1/filler/observe": {
      "post": {
        "tags": [
          "Streaming"
        ],
        "summary": "Publish a confirmed order transaction",
        "description": "Submit a transaction hash after confirmation. The API independently verifies its receipt and configured hook events, reads current order state from the hook, then briefly shows the result in the public open book and broadcasts an update. The indexer remains the durable source. This endpoint is a hint; callers cannot supply order data.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chainId",
                  "txHash"
                ],
                "properties": {
                  "chainId": {
                    "type": "integer"
                  },
                  "txHash": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{64}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Receipt verified and order change broadcast."
          },
          "400": {
            "description": "Invalid chain ID or transaction hash."
          },
          "429": {
            "description": "Observer is busy; retry shortly."
          },
          "503": {
            "description": "Receipt is unavailable or could not be verified yet."
          }
        }
      }
    },
    "/v1/filler/orders": {
      "get": {
        "tags": [
          "Filler"
        ],
        "summary": "List orders",
        "description": "Returns a page of orders ordered by `createdAt` desc. Filter by chain, status, owner, filler, exact pool, or token pair. Pair and pool filters apply before pagination.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ChainId"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "filled",
                "cancelled",
                "all"
              ],
              "default": "open"
            },
            "description": "Defaults to open, except when owner is supplied: then omitted status returns all statuses. Use all explicitly to disable the status filter."
          },
          {
            "name": "owner",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filler",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "poolId",
            "in": "query",
            "description": "Exact 32-byte pool ID. Takes precedence over the token-pair filter.",
            "schema": {
              "type": "string",
              "pattern": "^0x[0-9a-fA-F]{64}$"
            }
          },
          {
            "name": "tokenA",
            "in": "query",
            "description": "First token address. Supply together with tokenB to include every pool for this pair.",
            "schema": {
              "type": "string",
              "pattern": "^0x[0-9a-fA-F]{40}$"
            }
          },
          {
            "name": "tokenB",
            "in": "query",
            "description": "Second token address. Supply together with tokenA.",
            "schema": {
              "type": "string",
              "pattern": "^0x[0-9a-fA-F]{40}$"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Unix seconds. Only orders created at or after this time.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "orders",
                    "nextCursor"
                  ],
                  "properties": {
                    "orders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    },
                    "pageInfo": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "cursor": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/filler/orders/{orderId}": {
      "get": {
        "tags": [
          "Filler"
        ],
        "summary": "Get order",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrderId"
          },
          {
            "$ref": "#/components/parameters/ChainId"
          }
        ],
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/filler/orders/{orderId}/events": {
      "get": {
        "tags": [
          "Filler"
        ],
        "summary": "Get order events",
        "description": "Append-only log of every state change: `create`, one or more `price_update`s, terminated by `fill` or `cancel`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrderId"
          },
          {
            "$ref": "#/components/parameters/ChainId"
          }
        ],
        "responses": {
          "200": {
            "description": "Events in chronological order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "events"
                  ],
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrderEvent"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/filler/pools/{poolId}": {
      "get": {
        "tags": [
          "Filler"
        ],
        "summary": "Get pool metadata",
        "description": "Pool key, fee tier, hook address, and current price — joined with currency metadata for display.",
        "parameters": [
          {
            "name": "poolId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^0x[0-9a-fA-F]{64}$"
            }
          },
          {
            "$ref": "#/components/parameters/ChainId"
          }
        ],
        "responses": {
          "200": {
            "description": "Pool.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pool"
                }
              }
            }
          }
        }
      }
    },
    "/v1/filler/stats": {
      "get": {
        "tags": [
          "Filler"
        ],
        "summary": "Book statistics and protocol revenue",
        "description": "Headline numbers for one chain: open-order TVL, cumulative volume and filled volume as raw per-token sums (the client prices them), order/user counts, and what the protocol's FeeVault has earned. Every amount is a decimal string in the token's smallest units — never a JSON number.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ChainId"
          }
        ],
        "responses": {
          "200": {
            "description": "Statistics.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          }
        }
      }
    },
    "/v1/filler/stream": {
      "get": {
        "tags": [
          "Streaming"
        ],
        "summary": "WebSocket",
        "description": "GET against this path returns 426 Upgrade Required. Connect with a WebSocket client instead. See the **Streaming** tag for the message protocol.",
        "responses": {
          "426": {
            "description": "Upgrade to WebSocket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "ChainId": {
        "name": "chainId",
        "in": "query",
        "description": "EVM chain ID. Order data covers deployed Unichain and Arc mainnets and the four indexed testnets. Defaults to 130 (Unichain).",
        "schema": {
          "type": "integer",
          "default": 130
        }
      },
      "OrderId": {
        "name": "orderId",
        "in": "path",
        "required": true,
        "description": "32-byte order ID returned by the API. Current salt-based contracts use `keccak256(abi.encode(chainId, poolId, owner, zeroForOne, salt))`. Legacy contracts use their original nonce-based identity; do not recompute them using the current formula.",
        "schema": {
          "type": "string",
          "pattern": "^0x[0-9a-fA-F]{64}$"
        },
        "example": "0x0000000000000000000000000000000000000000000000000000000000000001"
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "Opaque cursor from a previous response's `nextCursor`.",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Order": {
        "type": "object",
        "description": "An Async Swap limit order. Salt and nonce depend on the originating deployment; neither is universally required.",
        "required": [
          "chainId",
          "orderId",
          "poolId",
          "owner",
          "zeroForOne",
          "amountIn",
          "amountOutMin",
          "status",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "chainId": {
            "type": "integer"
          },
          "orderId": {
            "type": "string",
            "description": "32-byte hex."
          },
          "poolId": {
            "type": "string",
            "description": "32-byte v4 pool key hash."
          },
          "owner": {
            "type": "string",
            "description": "Maker address."
          },
          "zeroForOne": {
            "type": "boolean",
            "description": "`true` if selling currency0 for currency1."
          },
          "amountIn": {
            "type": "string",
            "description": "Maker input in that token's smallest units, as a decimal string. This is what the filler receives."
          },
          "amountOutMin": {
            "type": "string",
            "description": "Maker's net minimum output in that token's smallest units. The filler pays this amount plus the live protocol fee."
          },
          "nonce": {
            "type": "string",
            "description": "Legacy order nonce; omitted for salt-only orders."
          },
          "salt": {
            "type": "string",
            "pattern": "^0x[0-9a-fA-F]{64}$",
            "description": "Contract-assigned salt for current orders; omitted for legacy orders."
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "filled",
              "cancelled"
            ]
          },
          "filler": {
            "type": "string",
            "nullable": true
          },
          "filledAt": {
            "type": "integer",
            "nullable": true,
            "description": "Unix seconds."
          },
          "cancelledAt": {
            "type": "integer",
            "nullable": true
          },
          "createTxHash": {
            "type": "string",
            "nullable": true
          },
          "fillTxHash": {
            "type": "string",
            "nullable": true
          },
          "cancelTxHash": {
            "type": "string",
            "nullable": true
          },
          "priceUpdateTxHash": {
            "type": "string",
            "nullable": true,
            "description": "Most recent price-update tx; full history via /events."
          },
          "createdAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds, or null if unavailable."
          },
          "updatedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds, or null if unavailable."
          }
        }
      },
      "OrderEvent": {
        "type": "object",
        "description": "One state change in an order's lifecycle.",
        "required": [
          "id",
          "chainId",
          "orderId",
          "kind",
          "txHash",
          "blockNumber",
          "timestamp"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "chainId": {
            "type": "integer"
          },
          "orderId": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "create",
              "price_update",
              "fill",
              "cancel"
            ]
          },
          "txHash": {
            "type": "string"
          },
          "blockNumber": {
            "type": "integer"
          },
          "timestamp": {
            "type": "integer"
          },
          "oldAmountOutMin": {
            "type": "string",
            "nullable": true,
            "description": "Set on `price_update` events only."
          },
          "newAmountOutMin": {
            "type": "string",
            "nullable": true
          },
          "filler": {
            "type": "string",
            "nullable": true,
            "description": "Set on `fill` events only."
          }
        }
      },
      "Pool": {
        "type": "object",
        "required": [
          "poolId",
          "chainId",
          "currency0",
          "currency1",
          "fee",
          "tickSpacing",
          "hooks",
          "sqrtPriceX96",
          "tick"
        ],
        "properties": {
          "poolId": {
            "type": "string"
          },
          "chainId": {
            "type": "integer"
          },
          "currency0": {
            "$ref": "#/components/schemas/Currency"
          },
          "currency1": {
            "$ref": "#/components/schemas/Currency"
          },
          "fee": {
            "type": "integer",
            "description": "Hundredths of a bip (e.g. 3000 = 0.3%)."
          },
          "tickSpacing": {
            "type": "integer"
          },
          "hooks": {
            "type": "string",
            "description": "Hook contract address."
          },
          "sqrtPriceX96": {
            "type": "string"
          },
          "tick": {
            "type": "integer"
          }
        }
      },
      "Stats": {
        "type": "object",
        "required": [
          "chainId",
          "counts",
          "tvl",
          "volume",
          "filled",
          "protocolRevenue"
        ],
        "properties": {
          "chainId": {
            "type": "integer"
          },
          "counts": {
            "type": "object",
            "required": [
              "protocolUsers",
              "openOrders",
              "ordersFilled"
            ],
            "properties": {
              "protocolUsers": {
                "type": "integer"
              },
              "openOrders": {
                "type": "integer"
              },
              "ordersFilled": {
                "type": "integer"
              }
            }
          },
          "tvl": {
            "type": "array",
            "description": "Input locked in OPEN Async Swap orders, per token.",
            "items": {
              "$ref": "#/components/schemas/TokenAmount"
            }
          },
          "volume": {
            "type": "array",
            "description": "All Async Swap orders plus recorded Uniswap swaps, per input token.",
            "items": {
              "$ref": "#/components/schemas/TokenAmount"
            }
          },
          "filled": {
            "type": "array",
            "description": "Filled Async Swap orders plus filled Uniswap swaps, per input token.",
            "items": {
              "$ref": "#/components/schemas/TokenAmount"
            }
          },
          "protocolRevenue": {
            "$ref": "#/components/schemas/ProtocolRevenue"
          }
        }
      },
      "TokenAmount": {
        "type": "object",
        "required": [
          "token",
          "symbol",
          "decimals",
          "amountIn",
          "count"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Token address, lowercase."
          },
          "symbol": {
            "type": "string",
            "description": "Empty when the token is not yet enriched."
          },
          "decimals": {
            "type": "integer"
          },
          "amountIn": {
            "type": "string",
            "description": "Decimal string, smallest units."
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "ProtocolRevenue": {
        "type": "object",
        "description": "What the V2 hook's FeeVault has earned on this chain, from the `OrderFeeAccrued` and `PriorityFeeForfeited` events. The v1.1 hook (chain 130) emits no fee events, so Unichain always reports zeros. Amounts are decimal strings in the token's smallest units.",
        "required": [
          "available",
          "fillFees",
          "priorityFeesForfeited",
          "priorityFeesDonated"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "`false` when this deployment's indexer does not index the fee events yet; the totals are then zeros, not a measurement."
          },
          "fillFees": {
            "type": "array",
            "description": "0.3% of each filled order's gross output, grouped by the OUTPUT currency it was taken in (`OrderFeeAccrued`).",
            "items": {
              "type": "object",
              "required": [
                "currency",
                "symbol",
                "decimals",
                "amount",
                "count"
              ],
              "properties": {
                "currency": {
                  "type": "string",
                  "description": "Token address, lowercase."
                },
                "symbol": {
                  "type": "string"
                },
                "decimals": {
                  "type": "integer"
                },
                "amount": {
                  "type": "string",
                  "description": "Decimal string, smallest units."
                },
                "count": {
                  "type": "integer",
                  "description": "Number of fee events."
                }
              }
            }
          },
          "priorityFeesForfeited": {
            "description": "Priority bids kept by the protocol because the pool had no active LP liquidity to donate to (`PriorityFeeForfeited`). Denominated in the hook numeraire (USDC on Arc).",
            "$ref": "#/components/schemas/FeeTotal"
          },
          "priorityFeesDonated": {
            "description": "Priority bids donated to the pool's LPs (`PriorityFeeDonated`). Reported for completeness; this is NOT protocol revenue. Same numeraire units as `priorityFeesForfeited`.",
            "$ref": "#/components/schemas/FeeTotal"
          }
        }
      },
      "FeeTotal": {
        "type": "object",
        "required": [
          "amount",
          "count"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "description": "Decimal string, smallest units."
          },
          "count": {
            "type": "integer",
            "description": "Number of events."
          }
        }
      },
      "Currency": {
        "type": "object",
        "required": [
          "address",
          "symbol",
          "decimals"
        ],
        "properties": {
          "address": {
            "type": "string"
          },
          "symbol": {
            "type": "string"
          },
          "decimals": {
            "type": "integer"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}