Skip to main content

ponk (Python)

A thin client for the ponk public API: create and drive Solana DLMM liquidity agents, read their live positions, PnL and fees, and move funds home.

Standard library only. No dependencies, no code generation, one method per endpoint. It never computes a number the server did not send.

Install

pip install ponk

0.5.0 adds the X Tracker, the Wallet Tracker, live agents activity, the weekly Ponkillionaires race and referral tiers (see Changes in 0.5.0). To install from this repository instead:

git clone https://github.com/ponkexchange/ponk-sdk
pip install ./ponk-sdk/python

or put the python directory on PYTHONPATH and import ponk.

Python 3.8 or newer.

Webhooks

ponk POSTs a signed JSON body to a URL you register. Verify it before you act on it:

from ponk import verify, InvalidSignature

@app.post("/ponk")
def receive(request):
    try:
        event = verify(
            raw_body=request.get_data(),               # BYTES, as received
            signature_header=request.headers["X-Ponk-Signature"],
            secret=MY_WEBHOOK_SECRET,
        )
    except InvalidSignature:
        return "", 400
    if event.event == "agent_out_of_range":
        page_someone(event.agent_id)
    return "", 200        # anything but 2xx is a failure, and ponk retries

Verify the raw bytes you received. The signature covers the exact body on the wire, and json.dumps(json.loads(body)) is not guaranteed to reproduce it, so verifying a re-serialized dict fails for reasons that look like a ponk bug and are not.

verify checks the HMAC and the age of the delivery, and raises InvalidSignature with a message saying which of the two failed. The timestamp is inside the MAC, so a captured delivery cannot be aged forward.

Registering endpoints is not in this client, and that is deliberate: the API scopes it to a signed-in session rather than to an API key, exactly as it does for minting keys. Register them in the app, under Settings, then verify here.

Get a key

Open Settings, API keys in the app with your wallet. Name the key, pick Read only (read) or Read and act (trade), and copy the secret. It is shown once: the server stores only a hash of it, so it genuinely cannot be shown again. Lost keys get revoked and replaced, not recovered.

A key carries exactly the authority of the wallet that minted it. It can never see or touch another account, and it cannot mint or revoke keys, so a leaked key cannot extend or outlive its own revocation.

Quickstart

import os
from ponk import PonkClient

ponk = PonkClient(api_key=os.environ["PONK_API_KEY"])

me = ponk.whoami()
print("acting as", me.wallet_address, "with scope", me.scope)

for agent in ponk.list_agents():
    print(agent.name, agent.status, agent.strategy, "dry_run" if agent.dry_run else "live")

    perf = ponk.agent_performance(agent.id)
    # Every USD field is a string or None. None means "could not be priced",
    # never zero, so print a dash rather than a number you do not have.
    print("  value", perf.current_value_usd or "-", "pnl", perf.pnl_usd or "-")

    pos = ponk.agent_position(agent.id)
    if pos.position_address:
        print("  range", pos.lower_price or "-", "to", pos.upper_price or "-",
              "in range" if pos.in_range else "OUT OF RANGE")

for log in ponk.list_logs(limit=20):
    print(log.created_at, log.action_type, log.status, log.tx_signature or "")

Create an agent, watch it think, then let it trade:

agent = ponk.create_agent(
    name="sol-usdc runner",
    wallet_address=me.wallet_address,   # must be the key's own wallet
    dex="meteora_dlmm",                 # meteora_dlmm | orca | ponk_clouds | raydium
    pool_address="POOL_ADDRESS",
    strategy="bin_rebalancer",
    config={"kind": "bin_rebalancer", "bin_range_width": 20, "rebalance_threshold_bins": 5},
    dry_run=True,                       # required: True simulates, False is live
)

# A dry-run agent runs its whole strategy loop and logs every decision to
# list_logs without sending a transaction. Read those logs before going live.
ponk.set_dry_run(agent.id, False)

dry_run is required by this client even though the server accepts it omitted, because on POST /v1/agents an omitted dry_run creates a live agent, the opposite of the app's default. A non-boolean raises TypeError before anything is sent. A live agent created here is still non-custodial: it recommends, and cannot sign for funds until a mandate signed in the app makes it autonomous.

For Degen Mode, strategy="entry_agent": the agent starts with no pool, so pool_address and position_address are ignored, and its config's venues must include one where ponk can discover pools, which today is only meteora_dlmm.

Price a wide Meteora range before you ask for one. The quote separates the rent you get back from the rent you do not:

q = ponk.range_cost("meteora_dlmm", "POOL_ADDRESS", bins=400)   # TOTAL bins
print(q.total_bins, "bins,", q.down_pct, "% to", q.up_pct, "%")
print("hold", q.total_upfront_lamports, "lamports;",
      q.non_refundable_lamports, "of it does not come back")
if q.clamped:
    print("the venue caps a position at", q.max_total_bins, "bins")

Move funds home:

wallet = ponk.agent_wallet(agent.id)
print(wallet.pubkey, wallet.lamports, "lamports")

ponk.withdraw(agent.id, lamports=500_000_000)   # 0.5 SOL, or omit to sweep
result = ponk.exit_agent(agent.id)              # stop, close, sweep everything
print("swept to", result.destination, "sig", result.signature)

What a key cannot do

Three limits are structural. They are properties of how custody works in ponk, not settings that can be turned off.

  • It cannot sign with your connected wallet. Non-custodial actions still need your signature in the app. This client can read their state, not produce the signature.
  • It cannot create a custody mandate. A mandate is a signature from your wallet, so it is granted in the app with your wallet connected. Once an agent has one, a trade key can use it: set_autonomous(id, True) turns autonomous signing on under that existing, unexpired mandate, and set_autonomous(id, False) turns it off, which is always allowed. This client never sends a grant, and the server refuses a key request that carries one.
  • It cannot act on an agent that holds no wallet of its own. compound, withdraw, exit and agent_wallet operate on the isolated wallet an autonomous agent owns. A self-custody agent has no such wallet, so those four do not apply to it. Reads, pause, resume, dry-run and mode do.

withdraw and exit have no destination parameter and cannot be given one. The server locks the destination to the wallet that owns the agent. A stolen key can move your funds home. It cannot move them anywhere else.

The mandate expires

A mandate carries a hard expiry, 30 days by default in the app. Lapsing is a clock fact with nothing on chain to mark it: the agent keeps running and every signature it asks for is denied, so it quietly stops compounding, claiming and recentering with its funds still deployed. Read the expiry instead of waiting for the silence:

from ponk import PonkNotFoundError

try:
    m = ponk.agent_mandate(agent.id)
except PonkNotFoundError:
    m = None            # no mandate: this agent is not autonomous

if m is not None:
    # seconds_remaining is computed by the server and goes negative once the
    # mandate has lapsed, so it does not depend on your clock.
    if m.status == "expired":
        print("expired", m.expires_at, "- renew it in the app")
    elif m.seconds_remaining < 3 * 86400:
        print("expires", m.expires_at, "- renew it in the app soon")
  • max_action_value_usd is None when there is no per-action ceiling. That means unlimited, not zero.
  • set_autonomous(id, False) does not revoke the mandate. A later set_autonomous(id, True) re-arms the agent while the mandate is unexpired. Enabling also sets the agent live, but it does not restart an agent a previous exit left stopped: read status on the result and call resume_agent if you need the loop running.
  • set_autonomous(id, True) needs the agent's own managed wallet with an active, unexpired mandate that still pays out to this account's wallet. Otherwise it raises PonkUnprocessableError with a sentence saying which.
  • compound can return a receipt and still have done nothing. A mandate denial is recorded on the action, not raised, so read signature is None as "nothing reached the chain" and error_message for why.
  • A lapsed mandate never traps funds. withdraw and exit deliberately bypass it and keep working after expiry.
  • ponk also sends mandate_expiring (from five days ahead) and mandate_expired notifications. A webhook registered from a signed-in session receives them; see Webhooks.

Methods

Method Endpoint Needs
health() GET /health nothing
pool(dex, address) GET /pools/{dex}/{address} nothing
range_cost(dex, address, bins) GET /pools/{dex}/{address}/range-cost?bins=N nothing
ponk_perks(wallet_address) GET /public/ponk/perks/{address} nothing
leaderboard(window=None) GET /public/agents-leaderboard?window=24h|7d|30d|all nothing
public_agent(slug) GET /public/agents/{slug} nothing
public_agent_value_history(slug, range=None) GET /public/agents/{slug}/value-history nothing
public_points_leaderboard(limit, offset) GET /public/points/leaderboard nothing
resolve_referral(code) GET /public/referral/{code} nothing
agents_activity() GET /public/agents-activity nothing
ponkillionaires_week() GET /public/ponkillionaires/week nothing
x_feed(before=, since=, limit=, category=, ca_only=, min_followers=, account=, q=) GET /public/x/feed nothing
x_trending(window=None, limit=None) GET /public/x/trending?window=1h|24h nothing
x_callers(days=None) GET /public/x/callers nothing
x_token(mint) GET /public/x/token/{mint} nothing
x_accounts() GET /public/x/accounts nothing
x_status() GET /public/x/status nothing
wallets_feed(before=, limit=, kind=, min_usd=, token=, wallet=) GET /public/wallets/feed nothing
wallets_defaults() GET /public/wallets/defaults nothing
wallets_status() GET /public/wallets/status nothing
wallet_activity(address, before=, limit=, kind=, min_usd=, token=) GET /public/wallets/{address}/activity nothing
whoami() GET /v1/me read
list_agents() GET /v1/agents read
get_agent(id) GET /v1/agents/{id} read
agent_performance(id) GET /v1/agents/{id}/performance read
agent_position(id) GET /v1/agents/{id}/position read
agent_value_history(id, range=None) GET /v1/agents/{id}/value-history read
agent_decisions(id, hours=None, limit=None) GET /v1/agents/{id}/decisions, raw JSON read
agent_wallet(id) GET /v1/agents/{id}/wallet read
agent_mandate(id) GET /v1/agents/{id}/mandate read
list_positions() GET /v1/positions read
list_logs(limit) GET /v1/logs (limit 1 to 500, default 100) read
get_public_link(id) GET /v1/agents/{id}/public-link read
list_follows() GET /v1/follows read
points() GET /v1/points read
points_leaderboard(limit, offset) GET /v1/points/leaderboard read
referral() GET /v1/referral read
creator_dashboard(days=None) GET /v1/referral/creator read
check_handle(handle) GET /v1/referral/handle/check read
get_json(path, params) any GET, raw JSON depends on the route
create_agent(...) POST /v1/agents trade
copy_agent(source, name=, wallet_address=, dry_run=) public_agent then POST /v1/agents from its copy block trade
optimize_draft(draft) POST /v1/agents/optimize-draft, raw JSON trade
pause_agent(id) POST /v1/agents/{id}/pause trade
resume_agent(id) POST /v1/agents/{id}/resume trade
set_autonomous(id, enabled) POST /v1/agents/{id}/autonomous trade
set_dry_run(id, dry_run) POST /v1/agents/{id}/dry-run trade
set_mode(id, mode) POST /v1/agents/{id}/mode trade
update_config(id, patch) PATCH /v1/agents/{id}/config trade
update_exits(id, tp_sl=None, automation=None) POST /v1/agents/{id}/exits trade
set_auto_compound(id, auto_compound, min_claimable_base_units=None, min_claimable_usd=None) POST /v1/agents/{id}/auto-compound trade
share_agent(id, hide_usd=False, show_onchain=None) POST /v1/agents/{id}/public-link trade
unshare_agent(id) DELETE /v1/agents/{id}/public-link trade
follow(slug) / unfollow(slug) POST / DELETE /v1/follows/{slug} trade
claim_handle(handle) POST /v1/referral/handle trade
compound(id) POST /v1/agents/{id}/compound trade
withdraw(id, lamports) POST /v1/agents/{id}/withdraw trade
exit_agent(id) POST /v1/agents/{id}/exit trade

Receiver-side, needing no key and no network:

Function Purpose
verify(raw_body, signature_header, secret) Check a webhook delivery and parse it
parse_signature_header(header) Split t=/v1= out of the header
EVENT_KINDS Every kind ponk sends, for recognition, never for rejection

The first four need no key at all. PonkClient() with no api_key reaches them and nothing else. pool and range_cost are rate limited per IP. health() returns its report even when the API answers 503, because which component is down is the whole point of that call.

This client covers the key-authenticated /v1 API plus those four public reads. The rest of the server's surface, the session-authenticated app routes and the wallet-signed Meteora transaction builders under /public/meteora, needs a wallet signature rather than a key, so it is out of scope here.

Errors

Every error carries the same envelope, and this client maps it by status:

{"error": {"code": "conflict", "message": "exit already running", "request_id": "..."}}
Status Exception What to do
400 PonkBadRequestError Fix the request. code is invalid_strategy_config when the config did not match the strategy, and message names the field.
401 PonkAuthError Missing, malformed, unknown or revoked key. The last two are deliberately indistinguishable. Do not retry.
403 PonkForbiddenError The key is valid but read-only. Mint a trade key. Do not retry.
404 PonkNotFoundError No such object, or it belongs to another account. Also deliberately indistinguishable.
409 PonkConflictError Conflicts with in-flight work, for example an exit already running. Retry after a pause.
422 PonkUnprocessableError Understood but cannot apply, for example compounding an agent with no open position. code carries the risk code when there is one.
429 PonkRateLimitedError Back off and retry. Only the public per-IP routes (pool, range_cost, ponk_perks) are rate limited; /v1 is not.
5xx PonkServerError Quote request_id to support.
no response PonkTransportError DNS, connection, TLS or timeout.

All of them subclass PonkError. PonkAPIError carries status, code, message, request_id and the decoded body.

from ponk import PonkAPIError, PonkConflictError, PonkTransportError

try:
    ponk.exit_agent(agent_id)
except PonkConflictError:
    pass                      # an exit is already running; it will finish
except PonkTransportError:
    # A fund call that times out has NOT necessarily failed: the work keeps
    # running on the server. Exit is idempotent, so call it again and it
    # continues rather than double-sending.
    ponk.exit_agent(agent_id)
except PonkAPIError as e:
    print(e.status, e.code, e.message, e.request_id)

compound, withdraw and exit send several transactions and wait for each confirmation, so they can take a minute. They get their own timeout, fund_timeout (180s), separate from timeout (30s) for everything else.

Reading the numbers

  • A null USD figure means unknown, not zero. An unpriceable pool returns null rather than a fabricated 0. Render it as a dash. Treating it as zero silently understates a position.
  • USD amounts on the agent and position endpoints are strings. Feed them to decimal.Decimal before doing arithmetic. They are strings precisely so a float cannot round them.
  • claimable_fees_usd and fee_pnl_usd are different numbers. The first is the unclaimed fees sitting on the position right now, the second is lifetime fees earned, claimed plus unclaimed. Do not add them.
  • PnL decomposes as Total = Price + Fee: pnl_usd, price_pnl_usd, fee_pnl_usd. pnl_source says whether the figures are Meteora's own (meteora_datapi) or ponk's on-chain valuation (oracle_estimate).
  • cost_basis_estimated marks a position ponk did not open. The basis is then a first-observation stamp, so lifetime PnL stays None instead of asserting a gain that contradicts your real numbers.
  • Every object keeps raw_json, the exact decoded payload, so a field added to the API after this client was written is still reachable.

Fees

Autonomous agents charge a performance fee on realized yield only. Principal is never touched and no fee is taken on an unrealized gain. There is no charge for a key, for a call, for creating an agent, for depositing or for withdrawing.

An agent created through this API pays the API rate, not the app rate. The rate is fixed when the agent is created and follows that agent for its whole life, including after you enable autonomous mode for it in the app. Read both rates live rather than hardcoding them:

perks = ponk.ponk_perks(me.wallet_address)
print("app agents", perks.managed_agent_fee.effective_percent, "%")
print("api agents", perks.api_agent_fee.effective_percent, "%")
print("holding $PONK would make that", perks.api_agent_fee.holder_percent, "%")

agent.managed_fee_bps is the rate that specific agent's skim actually charges, before any $PONK holder discount.

Changes in 0.5.0

  • Live agents activity, no key: agents_activity() returns the board agents' confirmed actions of the last 24 hours and every live hold, with no identity on any event.
  • Weekly Ponkillionaires, no key: ponkillionaires_week() returns this week's standings, the prizes (next week's agent fee rate plus points) and last week's winners.
  • X Tracker, no key: x_feed, x_trending, x_callers, x_token, x_accounts, x_status.
  • Wallet Tracker, no key: wallets_feed, wallets_defaults, wallets_status, and wallet_activity(address) for any wallet.
  • Referral tiers: referral() now carries tier (current, share_bps, referred_fees_usd, window_days, next) and programme (ladder, window_days). One level only: 20%, 25% from $1,000 and 30% from $5,000 of referred platform fees over 30 days.
  • The tracker and weekly race responses are camelCase on the wire; the models expose snake_case attributes (accounts_1h reads accounts1h).
  • PublicLink.path is documented as /ponkillionaires/<slug>, the path the server sends.

Not in the client, by design: following X accounts, tracked wallets, live copy follow, mandate reminder settings and the PnL and referral card settings are app session routes (/me/*, /agents/{id}/follow) that need a wallet sign-in, not an API key.

ponk = PonkClient()
hot = ponk.x_trending(window="24h", limit=10)
week = ponk.ponkillionaires_week()
tier = PonkClient(api_key=KEY).referral().tier
print(tier.current, tier.share_bps / 100, "%", tier.next.min_fees_usd if tier.next else "top")

Changes in 0.4.0

  • Top Ponkillionaires, no key: leaderboard(window), public_agent(slug), public_agent_value_history(slug, range).
  • Copy an agent: copy_agent(slug_or_row, name=, wallet_address=, dry_run=, config_overrides=None) creates an agent on your wallet from a shared agent's copy block.
  • Share and follow: share_agent, unshare_agent, get_public_link, follow, unfollow, list_follows.
  • Points and referrals: points, points_leaderboard, public_points_leaderboard, referral, creator_dashboard, check_handle, claim_handle, resolve_referral.
  • Agent reads: agent_value_history and agent_decisions.
  • Settings edits: update_config, update_exits, set_auto_compound, and optimize_draft. The docstrings list the new keys: keep side, reshape, cooldown and daily cap, the market cap, value and value plus fees take profits, and the Degen Mode bin step filter.
board = ponk.leaderboard(window="7d")
row = board.ranked[0]
mine = ponk.copy_agent(row, name="copy", wallet_address=me.wallet_address, dry_run=True)

standing = ponk.points()
print(standing.season.name if standing.season else "off-season", standing.total_points, standing.rank)
r = ponk.referral()
print("earned", r.total_earned_usd, "owed", r.owed_usd, "unproven", r.unproven_usd)  # never add these

Changes in 0.3.0

  • Breaking: create_agent requires dry_run. 0.2.0 documented an omitted dry_run as a dry run. On /v1 it is the opposite: an omitted dry_run creates a live agent. Code that relied on the old wording was creating live agents, so 0.3.0 makes the choice explicit and raises TypeError if it is missing or not a bool.
  • agent_mandate(id) reads the live custody mandate, including its expiry.
  • set_autonomous(id, enabled) turns autonomous signing on under an existing mandate, or off. It never creates a mandate.
  • range_cost(dex, address, bins) quotes what a Meteora range costs to open.
  • get_json(path, params) reaches any GET this client does not name.
  • The User-Agent now carries the real package version (it said 0.1.0).

Tests

Offline, no network, no fixtures to refresh:

cd python && python3 -m pytest

python3 -m unittest discover -s tests -v runs the same suite without pytest.

Metadata

Release files for ponk 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ponk 0.5.0
File Size Uploaded Provenance
ponk-0.5.0.tar.gz 60.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ponk 0.5.0
File Interpreter ABI Platform
ponk-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 102.9 kB

Release files / ponk-0.5.0.tar.gz

Download URL ponk-0.5.0.tar.gz
Size 60.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b73b4c782de84e3db8b81e17d1fa056135e9f413836abbb9173e5ef5867c647c
BLAKE2b-256 checksum
How to use checksums
5aa9adeffefe99f4c8e2461c980aeaec102a0497a39a99bcf67d604fbd12b5e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / ponk-0.5.0-py3-none-any.whl

Download URL ponk-0.5.0-py3-none-any.whl
Size 42.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d4370fd05cd381dd8f60e853396235e5964314063cecf204b25c1704e64af2fd
BLAKE2b-256 checksum
How to use checksums
4bef0616fbd790689ec793c22ee2e917dfeab592bb9485e61338c978cba13950
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Security for ponk 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page