Skip to content

pendle-py

CI PyPI Docs Python versions License: MIT

Typed Python client for the Pendle Finance v2 API. Read PT/YT/SY/LP markets, asset metadata and APY history, and build swap / liquidity / mint / redeem calldata — sync and async, with full type hints (py.typed), built on httpx + pydantic v2.

The API is public and keyless. Signing and broadcasting the returned transaction is left to you (optionally with the [exec] extra, which pulls in web3.py).

Install

pip install pendle-py
# optional signing/broadcasting helpers:
pip install "pendle-py[exec]"

Quickstart

List active Ethereum markets with their implied APY in a few lines:

from pendle import PendleClient
from pendle.constants import ChainId

with PendleClient() as client:
    active = client.get_active_markets(ChainId.ETHEREUM)
    for m in active.markets[:5]:
        implied = (m.details.implied_apy or 0) * 100
        print(f"{m.name:<12} implied APY {implied:5.2f}%  liquidity ${m.details.liquidity:,.0f}")

Build swap calldata

convert is Pendle's universal calldata endpoint; swap / add_liquidity / remove_liquidity / mint_py / redeem_py are ergonomic wrappers over it. The returned action (e.g. "swap") is inferred from the tokens you pass.

with PendleClient() as client:
    market = client.get_markets(ChainId.ETHEREUM, limit=1).results[0]

    resp = client.swap(
        ChainId.ETHEREUM,
        token_in=market.sy.address,        # spend SY
        amount_in="1000000000000000000",   # 1.0 (18 decimals), in wei
        token_out=market.pt.address,       # receive PT
        receiver="0xYourAddress",
        slippage=0.01,                     # 1%
    )

    print(resp.action)                     # "swap"
    for approval in resp.required_approvals:
        ...                                # grant ERC-20 approval first
    tx = resp.routes[0].tx                 # unsigned tx to sign & broadcast
    print(tx.to, tx.data, tx.value)

Async

import asyncio
from pendle import AsyncPendleClient

async def main():
    async with AsyncPendleClient() as client:
        markets = await client.get_markets(1, limit=5)
        print(markets.total)

asyncio.run(main())

API surface

Method Endpoint Returns
get_markets(chain_id, limit=, skip=) GET /v1/{chainId}/markets MarketsResponse
get_market(chain_id, address) GET /v1/{chainId}/markets/{address} Market
get_active_markets(chain_id) GET /v1/{chainId}/markets/active ActiveMarketsResponse
get_assets(chain_id) GET /v1/{chainId}/assets/all list[Asset]
get_historical_data(chain_id, address, time_frame=) GET /v3/{chainId}/markets/{address}/historical-data HistoricalDataResponse
convert(chain_id, *, receiver, slippage, inputs, outputs, ...) POST /v3/sdk/{chainId}/convert ConvertResponse
swap / add_liquidity / remove_liquidity / mint_py / redeem_py (wrappers over convert) ConvertResponse

All methods exist on both PendleClient (sync) and AsyncPendleClient (async). Every chain-id argument accepts a plain int or a pendle.constants.ChainId member.

Note on the SDK endpoint. Pendle consolidated its per-action SDK routes (separate swap / add-liquidity / mint / redeem endpoints) into a single POST /v3/sdk/{chainId}/convert. This library exposes that endpoint directly as convert() and adds the named wrappers above for convenience.

Errors

Every method raises PendleAPIError (a subclass of PendleError) on a non-2xx response, exposing .status_code, .error and .message:

from pendle import PendleClient, PendleAPIError

with PendleClient() as client:
    try:
        client.get_market(1, "0xnot-a-market")
    except PendleAPIError as exc:
        print(exc.status_code, exc.message)   # 404 'Not Found'

Network-level failures propagate as httpx exceptions.

Configuration

# Custom base URL (e.g. a proxy) and timeout, or inject your own httpx client:
PendleClient(base_url="https://my-proxy.example/core", timeout=15.0)
PendleClient(client=my_httpx_client)   # caller owns its lifecycle

Development

See CONTRIBUTING.md. In short:

uv venv --python 3.9 && source .venv/bin/activate
uv pip install -e ".[dev]"
ruff check . && ruff format --check . && mypy && pytest

Live integration tests are marked integration and deselected by default; run them with pytest -m integration.

License

MIT — see LICENSE.