ponk 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ponk-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: ponk
3
+ Version: 0.2.0
4
+ Summary: Python client for the ponk API: Solana DLMM liquidity agents, and webhook verification.
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://ponk.exchange
7
+ Project-URL: Documentation, https://ponk.exchange/docs/developers/agents-api
8
+ Project-URL: Source, https://github.com/ponkexchange/ponk-sdk
9
+ Project-URL: Issues, https://github.com/ponkexchange/ponk-sdk/issues
10
+ Keywords: solana,dlmm,liquidity,meteora,orca,ponk,webhooks
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ponk (Python)
23
+
24
+ A thin client for the ponk public API: create and drive Solana DLMM liquidity
25
+ agents, read their live positions, PnL and fees, and move funds home.
26
+
27
+ Standard library only. No dependencies, no code generation, one method per
28
+ endpoint. It never computes a number the server did not send.
29
+
30
+ ## Install
31
+
32
+ The package is not on PyPI. Use it from this repository:
33
+
34
+ ```bash
35
+ git clone https://github.com/ponkexchange/ponk-sdk
36
+ pip install ./ponk-sdk/python
37
+ ```
38
+
39
+ or put the `python` directory on `PYTHONPATH` and `import ponk`.
40
+
41
+ Python 3.8 or newer.
42
+
43
+ ## Webhooks
44
+
45
+ ponk POSTs a signed JSON body to a URL you register. Verify it before you
46
+ act on it:
47
+
48
+ ```python
49
+ from ponk import verify, InvalidSignature
50
+
51
+ @app.post("/ponk")
52
+ def receive(request):
53
+ try:
54
+ event = verify(
55
+ raw_body=request.get_data(), # BYTES, as received
56
+ signature_header=request.headers["X-Ponk-Signature"],
57
+ secret=MY_WEBHOOK_SECRET,
58
+ )
59
+ except InvalidSignature:
60
+ return "", 400
61
+ if event.event == "agent_out_of_range":
62
+ page_someone(event.agent_id)
63
+ return "", 200 # anything but 2xx is a failure, and ponk retries
64
+ ```
65
+
66
+ Verify the **raw bytes you received**. The signature covers the exact body on
67
+ the wire, and `json.dumps(json.loads(body))` is not guaranteed to reproduce
68
+ it, so verifying a re-serialized dict fails for reasons that look like a ponk
69
+ bug and are not.
70
+
71
+ `verify` checks the HMAC and the age of the delivery, and raises
72
+ `InvalidSignature` with a message saying which of the two failed. The
73
+ timestamp is inside the MAC, so a captured delivery cannot be aged forward.
74
+
75
+ Registering endpoints is not in this client, and that is deliberate: the API
76
+ scopes it to a signed-in session rather than to an API key, exactly as it does
77
+ for minting keys. Register them in the app, under Settings, then verify here.
78
+
79
+ ## Get a key
80
+
81
+ Open [Settings, API keys](https://ponk.exchange/settings) in the app with your
82
+ wallet. Name the key, pick **Read only** (`read`) or **Read and act**
83
+ (`trade`), and copy the secret. It is shown once: the server stores only a hash
84
+ of it, so it genuinely cannot be shown again. Lost keys get revoked and
85
+ replaced, not recovered.
86
+
87
+ A key carries exactly the authority of the wallet that minted it. It can never
88
+ see or touch another account, and it cannot mint or revoke keys, so a leaked
89
+ key cannot extend or outlive its own revocation.
90
+
91
+ ## Quickstart
92
+
93
+ ```python
94
+ import os
95
+ from ponk import PonkClient
96
+
97
+ ponk = PonkClient(api_key=os.environ["PONK_API_KEY"])
98
+
99
+ me = ponk.whoami()
100
+ print("acting as", me.wallet_address, "with scope", me.scope)
101
+
102
+ for agent in ponk.list_agents():
103
+ print(agent.name, agent.status, agent.strategy, "dry_run" if agent.dry_run else "live")
104
+
105
+ perf = ponk.agent_performance(agent.id)
106
+ # Every USD field is a string or None. None means "could not be priced",
107
+ # never zero, so print a dash rather than a number you do not have.
108
+ print(" value", perf.current_value_usd or "-", "pnl", perf.pnl_usd or "-")
109
+
110
+ pos = ponk.agent_position(agent.id)
111
+ if pos.position_address:
112
+ print(" range", pos.lower_price or "-", "to", pos.upper_price or "-",
113
+ "in range" if pos.in_range else "OUT OF RANGE")
114
+
115
+ for log in ponk.list_logs(limit=20):
116
+ print(log.created_at, log.action_type, log.status, log.tx_signature or "")
117
+ ```
118
+
119
+ Create an agent, watch it think, then let it trade:
120
+
121
+ ```python
122
+ agent = ponk.create_agent(
123
+ name="sol-usdc runner",
124
+ wallet_address=me.wallet_address, # must be the key's own wallet
125
+ dex="meteora_dlmm", # meteora_dlmm | orca | ponk_clouds
126
+ pool_address="POOL_ADDRESS",
127
+ strategy="bin_rebalancer",
128
+ config={"kind": "bin_rebalancer", "bin_range_width": 20, "rebalance_threshold_bins": 5},
129
+ dry_run=True, # the server's default is also True
130
+ )
131
+
132
+ # A dry-run agent runs its whole strategy loop and logs every decision to
133
+ # list_logs without sending a transaction. Read those logs before going live.
134
+ ponk.set_dry_run(agent.id, False)
135
+ ```
136
+
137
+ Move funds home:
138
+
139
+ ```python
140
+ wallet = ponk.agent_wallet(agent.id)
141
+ print(wallet.pubkey, wallet.lamports, "lamports")
142
+
143
+ ponk.withdraw(agent.id, lamports=500_000_000) # 0.5 SOL, or omit to sweep
144
+ result = ponk.exit_agent(agent.id) # stop, close, sweep everything
145
+ print("swept to", result.destination, "sig", result.signature)
146
+ ```
147
+
148
+ ## What a key cannot do
149
+
150
+ Three limits are structural. They are properties of how custody works in ponk,
151
+ not settings that can be turned off.
152
+
153
+ * **It cannot sign with your connected wallet.** Non-custodial actions still
154
+ need your signature in the app. This client can read their state, not produce
155
+ the signature.
156
+ * **It cannot grant itself custody.** Turning an agent autonomous requires a
157
+ one-time custody mandate you sign with your wallet. Create the agent here if
158
+ you like, then enable autonomous mode once in the app; from then on this
159
+ client can drive it.
160
+ * **It cannot act on an agent that holds no wallet of its own.** `compound`,
161
+ `withdraw`, `exit` and `agent_wallet` operate on the isolated wallet an
162
+ autonomous agent owns. A self-custody agent has no such wallet, so those four
163
+ do not apply to it. Reads, pause, resume, dry-run and mode do.
164
+
165
+ `withdraw` and `exit` have no destination parameter and cannot be given one.
166
+ The server locks the destination to the wallet that owns the agent. A stolen
167
+ key can move your funds home. It cannot move them anywhere else.
168
+
169
+ ## Methods
170
+
171
+ | Method | Endpoint | Needs |
172
+ | --- | --- | --- |
173
+ | `health()` | `GET /health` | nothing |
174
+ | `pool(dex, address)` | `GET /pools/{dex}/{address}` | nothing |
175
+ | `ponk_perks(wallet_address)` | `GET /public/ponk/perks/{address}` | nothing |
176
+ | `whoami()` | `GET /v1/me` | `read` |
177
+ | `list_agents()` | `GET /v1/agents` | `read` |
178
+ | `get_agent(id)` | `GET /v1/agents/{id}` | `read` |
179
+ | `agent_performance(id)` | `GET /v1/agents/{id}/performance` | `read` |
180
+ | `agent_position(id)` | `GET /v1/agents/{id}/position` | `read` |
181
+ | `agent_wallet(id)` | `GET /v1/agents/{id}/wallet` | `read` |
182
+ | `list_positions()` | `GET /v1/positions` | `read` |
183
+ | `list_logs(limit)` | `GET /v1/logs` | `read` |
184
+ | `create_agent(...)` | `POST /v1/agents` | `trade` |
185
+ | `pause_agent(id)` | `POST /v1/agents/{id}/pause` | `trade` |
186
+ | `resume_agent(id)` | `POST /v1/agents/{id}/resume` | `trade` |
187
+ | `set_dry_run(id, dry_run)` | `POST /v1/agents/{id}/dry-run` | `trade` |
188
+ | `set_mode(id, mode)` | `POST /v1/agents/{id}/mode` | `trade` |
189
+ | `compound(id)` | `POST /v1/agents/{id}/compound` | `trade` |
190
+ | `withdraw(id, lamports)` | `POST /v1/agents/{id}/withdraw` | `trade` |
191
+ | `exit_agent(id)` | `POST /v1/agents/{id}/exit` | `trade` |
192
+
193
+ Receiver-side, needing no key and no network:
194
+
195
+ | Function | Purpose |
196
+ | --- | --- |
197
+ | `verify(raw_body, signature_header, secret)` | Check a webhook delivery and parse it |
198
+ | `parse_signature_header(header)` | Split `t=`/`v1=` out of the header |
199
+ | `EVENT_KINDS` | Every kind ponk sends, for recognition, never for rejection |
200
+
201
+ The first three need no key at all. `PonkClient()` with no `api_key` reaches
202
+ them and nothing else. `health()` returns its report even when the API answers
203
+ 503, because which component is down is the whole point of that call.
204
+
205
+ This client covers the key-authenticated `/v1` API plus those three public
206
+ reads. The rest of the server's surface, the session-authenticated app routes
207
+ and the wallet-signed Meteora transaction builders under `/public/meteora`,
208
+ needs a wallet signature rather than a key, so it is out of scope here.
209
+
210
+ ## Errors
211
+
212
+ Every error carries the same envelope, and this client maps it by status:
213
+
214
+ ```json
215
+ {"error": {"code": "conflict", "message": "exit already running", "request_id": "..."}}
216
+ ```
217
+
218
+ | Status | Exception | What to do |
219
+ | --- | --- | --- |
220
+ | 400 | `PonkBadRequestError` | Fix the request. `code` is `invalid_strategy_config` when the config did not match the strategy, and `message` names the field. |
221
+ | 401 | `PonkAuthError` | Missing, malformed, unknown or revoked key. The last two are deliberately indistinguishable. Do not retry. |
222
+ | 403 | `PonkForbiddenError` | The key is valid but read-only. Mint a `trade` key. Do not retry. |
223
+ | 404 | `PonkNotFoundError` | No such object, or it belongs to another account. Also deliberately indistinguishable. |
224
+ | 409 | `PonkConflictError` | Conflicts with in-flight work, for example an exit already running. Retry after a pause. |
225
+ | 422 | `PonkUnprocessableError` | Understood but cannot apply, for example compounding an agent with no open position. `code` carries the risk code when there is one. |
226
+ | 429 | `PonkRateLimitedError` | Back off and retry. |
227
+ | 5xx | `PonkServerError` | Quote `request_id` to support. |
228
+ | no response | `PonkTransportError` | DNS, connection, TLS or timeout. |
229
+
230
+ All of them subclass `PonkError`. `PonkAPIError` carries `status`, `code`,
231
+ `message`, `request_id` and the decoded `body`.
232
+
233
+ ```python
234
+ from ponk import PonkAPIError, PonkConflictError, PonkTransportError
235
+
236
+ try:
237
+ ponk.exit_agent(agent_id)
238
+ except PonkConflictError:
239
+ pass # an exit is already running; it will finish
240
+ except PonkTransportError:
241
+ # A fund call that times out has NOT necessarily failed: the work keeps
242
+ # running on the server. Exit is idempotent, so call it again and it
243
+ # continues rather than double-sending.
244
+ ponk.exit_agent(agent_id)
245
+ except PonkAPIError as e:
246
+ print(e.status, e.code, e.message, e.request_id)
247
+ ```
248
+
249
+ `compound`, `withdraw` and `exit` send several transactions and wait for each
250
+ confirmation, so they can take a minute. They get their own timeout,
251
+ `fund_timeout` (180s), separate from `timeout` (30s) for everything else.
252
+
253
+ ## Reading the numbers
254
+
255
+ * **A null USD figure means unknown, not zero.** An unpriceable pool returns
256
+ `null` rather than a fabricated 0. Render it as a dash. Treating it as zero
257
+ silently understates a position.
258
+ * **USD amounts on the agent and position endpoints are strings.** Feed them to
259
+ `decimal.Decimal` before doing arithmetic. They are strings precisely so a
260
+ float cannot round them.
261
+ * **`claimable_fees_usd` and `fee_pnl_usd` are different numbers.** The first is
262
+ the unclaimed fees sitting on the position right now, the second is lifetime
263
+ fees earned, claimed plus unclaimed. Do not add them.
264
+ * **PnL decomposes as Total = Price + Fee**: `pnl_usd`, `price_pnl_usd`,
265
+ `fee_pnl_usd`. `pnl_source` says whether the figures are Meteora's own
266
+ (`meteora_datapi`) or ponk's on-chain valuation (`oracle_estimate`).
267
+ * **`cost_basis_estimated`** marks a position ponk did not open. The basis is
268
+ then a first-observation stamp, so lifetime PnL stays `None` instead of
269
+ asserting a gain that contradicts your real numbers.
270
+ * **Every object keeps `raw_json`**, the exact decoded payload, so a field added
271
+ to the API after this client was written is still reachable.
272
+
273
+ ## Fees
274
+
275
+ Autonomous agents charge a performance fee on realized yield only. Principal is
276
+ never touched and no fee is taken on an unrealized gain. There is no charge for
277
+ a key, for a call, for creating an agent, for depositing or for withdrawing.
278
+
279
+ **An agent created through this API pays the API rate, not the app rate.** The
280
+ rate is fixed when the agent is created and follows that agent for its whole
281
+ life, including after you enable autonomous mode for it in the app. Read both
282
+ rates live rather than hardcoding them:
283
+
284
+ ```python
285
+ perks = ponk.ponk_perks(me.wallet_address)
286
+ print("app agents", perks.managed_agent_fee.effective_percent, "%")
287
+ print("api agents", perks.api_agent_fee.effective_percent, "%")
288
+ print("holding $PONK would make that", perks.api_agent_fee.holder_percent, "%")
289
+ ```
290
+
291
+ `agent.managed_fee_bps` is the rate that specific agent's skim actually
292
+ charges, before any $PONK holder discount.
293
+
294
+ ## Tests
295
+
296
+ Offline, no network, no fixtures to refresh:
297
+
298
+ ```bash
299
+ cd packages/sdk-python && python3 -m unittest discover -s tests -v
300
+ ```
ponk-0.2.0/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # ponk (Python)
2
+
3
+ A thin client for the ponk public API: create and drive Solana DLMM liquidity
4
+ agents, read their live positions, PnL and fees, and move funds home.
5
+
6
+ Standard library only. No dependencies, no code generation, one method per
7
+ endpoint. It never computes a number the server did not send.
8
+
9
+ ## Install
10
+
11
+ The package is not on PyPI. Use it from this repository:
12
+
13
+ ```bash
14
+ git clone https://github.com/ponkexchange/ponk-sdk
15
+ pip install ./ponk-sdk/python
16
+ ```
17
+
18
+ or put the `python` directory on `PYTHONPATH` and `import ponk`.
19
+
20
+ Python 3.8 or newer.
21
+
22
+ ## Webhooks
23
+
24
+ ponk POSTs a signed JSON body to a URL you register. Verify it before you
25
+ act on it:
26
+
27
+ ```python
28
+ from ponk import verify, InvalidSignature
29
+
30
+ @app.post("/ponk")
31
+ def receive(request):
32
+ try:
33
+ event = verify(
34
+ raw_body=request.get_data(), # BYTES, as received
35
+ signature_header=request.headers["X-Ponk-Signature"],
36
+ secret=MY_WEBHOOK_SECRET,
37
+ )
38
+ except InvalidSignature:
39
+ return "", 400
40
+ if event.event == "agent_out_of_range":
41
+ page_someone(event.agent_id)
42
+ return "", 200 # anything but 2xx is a failure, and ponk retries
43
+ ```
44
+
45
+ Verify the **raw bytes you received**. The signature covers the exact body on
46
+ the wire, and `json.dumps(json.loads(body))` is not guaranteed to reproduce
47
+ it, so verifying a re-serialized dict fails for reasons that look like a ponk
48
+ bug and are not.
49
+
50
+ `verify` checks the HMAC and the age of the delivery, and raises
51
+ `InvalidSignature` with a message saying which of the two failed. The
52
+ timestamp is inside the MAC, so a captured delivery cannot be aged forward.
53
+
54
+ Registering endpoints is not in this client, and that is deliberate: the API
55
+ scopes it to a signed-in session rather than to an API key, exactly as it does
56
+ for minting keys. Register them in the app, under Settings, then verify here.
57
+
58
+ ## Get a key
59
+
60
+ Open [Settings, API keys](https://ponk.exchange/settings) in the app with your
61
+ wallet. Name the key, pick **Read only** (`read`) or **Read and act**
62
+ (`trade`), and copy the secret. It is shown once: the server stores only a hash
63
+ of it, so it genuinely cannot be shown again. Lost keys get revoked and
64
+ replaced, not recovered.
65
+
66
+ A key carries exactly the authority of the wallet that minted it. It can never
67
+ see or touch another account, and it cannot mint or revoke keys, so a leaked
68
+ key cannot extend or outlive its own revocation.
69
+
70
+ ## Quickstart
71
+
72
+ ```python
73
+ import os
74
+ from ponk import PonkClient
75
+
76
+ ponk = PonkClient(api_key=os.environ["PONK_API_KEY"])
77
+
78
+ me = ponk.whoami()
79
+ print("acting as", me.wallet_address, "with scope", me.scope)
80
+
81
+ for agent in ponk.list_agents():
82
+ print(agent.name, agent.status, agent.strategy, "dry_run" if agent.dry_run else "live")
83
+
84
+ perf = ponk.agent_performance(agent.id)
85
+ # Every USD field is a string or None. None means "could not be priced",
86
+ # never zero, so print a dash rather than a number you do not have.
87
+ print(" value", perf.current_value_usd or "-", "pnl", perf.pnl_usd or "-")
88
+
89
+ pos = ponk.agent_position(agent.id)
90
+ if pos.position_address:
91
+ print(" range", pos.lower_price or "-", "to", pos.upper_price or "-",
92
+ "in range" if pos.in_range else "OUT OF RANGE")
93
+
94
+ for log in ponk.list_logs(limit=20):
95
+ print(log.created_at, log.action_type, log.status, log.tx_signature or "")
96
+ ```
97
+
98
+ Create an agent, watch it think, then let it trade:
99
+
100
+ ```python
101
+ agent = ponk.create_agent(
102
+ name="sol-usdc runner",
103
+ wallet_address=me.wallet_address, # must be the key's own wallet
104
+ dex="meteora_dlmm", # meteora_dlmm | orca | ponk_clouds
105
+ pool_address="POOL_ADDRESS",
106
+ strategy="bin_rebalancer",
107
+ config={"kind": "bin_rebalancer", "bin_range_width": 20, "rebalance_threshold_bins": 5},
108
+ dry_run=True, # the server's default is also True
109
+ )
110
+
111
+ # A dry-run agent runs its whole strategy loop and logs every decision to
112
+ # list_logs without sending a transaction. Read those logs before going live.
113
+ ponk.set_dry_run(agent.id, False)
114
+ ```
115
+
116
+ Move funds home:
117
+
118
+ ```python
119
+ wallet = ponk.agent_wallet(agent.id)
120
+ print(wallet.pubkey, wallet.lamports, "lamports")
121
+
122
+ ponk.withdraw(agent.id, lamports=500_000_000) # 0.5 SOL, or omit to sweep
123
+ result = ponk.exit_agent(agent.id) # stop, close, sweep everything
124
+ print("swept to", result.destination, "sig", result.signature)
125
+ ```
126
+
127
+ ## What a key cannot do
128
+
129
+ Three limits are structural. They are properties of how custody works in ponk,
130
+ not settings that can be turned off.
131
+
132
+ * **It cannot sign with your connected wallet.** Non-custodial actions still
133
+ need your signature in the app. This client can read their state, not produce
134
+ the signature.
135
+ * **It cannot grant itself custody.** Turning an agent autonomous requires a
136
+ one-time custody mandate you sign with your wallet. Create the agent here if
137
+ you like, then enable autonomous mode once in the app; from then on this
138
+ client can drive it.
139
+ * **It cannot act on an agent that holds no wallet of its own.** `compound`,
140
+ `withdraw`, `exit` and `agent_wallet` operate on the isolated wallet an
141
+ autonomous agent owns. A self-custody agent has no such wallet, so those four
142
+ do not apply to it. Reads, pause, resume, dry-run and mode do.
143
+
144
+ `withdraw` and `exit` have no destination parameter and cannot be given one.
145
+ The server locks the destination to the wallet that owns the agent. A stolen
146
+ key can move your funds home. It cannot move them anywhere else.
147
+
148
+ ## Methods
149
+
150
+ | Method | Endpoint | Needs |
151
+ | --- | --- | --- |
152
+ | `health()` | `GET /health` | nothing |
153
+ | `pool(dex, address)` | `GET /pools/{dex}/{address}` | nothing |
154
+ | `ponk_perks(wallet_address)` | `GET /public/ponk/perks/{address}` | nothing |
155
+ | `whoami()` | `GET /v1/me` | `read` |
156
+ | `list_agents()` | `GET /v1/agents` | `read` |
157
+ | `get_agent(id)` | `GET /v1/agents/{id}` | `read` |
158
+ | `agent_performance(id)` | `GET /v1/agents/{id}/performance` | `read` |
159
+ | `agent_position(id)` | `GET /v1/agents/{id}/position` | `read` |
160
+ | `agent_wallet(id)` | `GET /v1/agents/{id}/wallet` | `read` |
161
+ | `list_positions()` | `GET /v1/positions` | `read` |
162
+ | `list_logs(limit)` | `GET /v1/logs` | `read` |
163
+ | `create_agent(...)` | `POST /v1/agents` | `trade` |
164
+ | `pause_agent(id)` | `POST /v1/agents/{id}/pause` | `trade` |
165
+ | `resume_agent(id)` | `POST /v1/agents/{id}/resume` | `trade` |
166
+ | `set_dry_run(id, dry_run)` | `POST /v1/agents/{id}/dry-run` | `trade` |
167
+ | `set_mode(id, mode)` | `POST /v1/agents/{id}/mode` | `trade` |
168
+ | `compound(id)` | `POST /v1/agents/{id}/compound` | `trade` |
169
+ | `withdraw(id, lamports)` | `POST /v1/agents/{id}/withdraw` | `trade` |
170
+ | `exit_agent(id)` | `POST /v1/agents/{id}/exit` | `trade` |
171
+
172
+ Receiver-side, needing no key and no network:
173
+
174
+ | Function | Purpose |
175
+ | --- | --- |
176
+ | `verify(raw_body, signature_header, secret)` | Check a webhook delivery and parse it |
177
+ | `parse_signature_header(header)` | Split `t=`/`v1=` out of the header |
178
+ | `EVENT_KINDS` | Every kind ponk sends, for recognition, never for rejection |
179
+
180
+ The first three need no key at all. `PonkClient()` with no `api_key` reaches
181
+ them and nothing else. `health()` returns its report even when the API answers
182
+ 503, because which component is down is the whole point of that call.
183
+
184
+ This client covers the key-authenticated `/v1` API plus those three public
185
+ reads. The rest of the server's surface, the session-authenticated app routes
186
+ and the wallet-signed Meteora transaction builders under `/public/meteora`,
187
+ needs a wallet signature rather than a key, so it is out of scope here.
188
+
189
+ ## Errors
190
+
191
+ Every error carries the same envelope, and this client maps it by status:
192
+
193
+ ```json
194
+ {"error": {"code": "conflict", "message": "exit already running", "request_id": "..."}}
195
+ ```
196
+
197
+ | Status | Exception | What to do |
198
+ | --- | --- | --- |
199
+ | 400 | `PonkBadRequestError` | Fix the request. `code` is `invalid_strategy_config` when the config did not match the strategy, and `message` names the field. |
200
+ | 401 | `PonkAuthError` | Missing, malformed, unknown or revoked key. The last two are deliberately indistinguishable. Do not retry. |
201
+ | 403 | `PonkForbiddenError` | The key is valid but read-only. Mint a `trade` key. Do not retry. |
202
+ | 404 | `PonkNotFoundError` | No such object, or it belongs to another account. Also deliberately indistinguishable. |
203
+ | 409 | `PonkConflictError` | Conflicts with in-flight work, for example an exit already running. Retry after a pause. |
204
+ | 422 | `PonkUnprocessableError` | Understood but cannot apply, for example compounding an agent with no open position. `code` carries the risk code when there is one. |
205
+ | 429 | `PonkRateLimitedError` | Back off and retry. |
206
+ | 5xx | `PonkServerError` | Quote `request_id` to support. |
207
+ | no response | `PonkTransportError` | DNS, connection, TLS or timeout. |
208
+
209
+ All of them subclass `PonkError`. `PonkAPIError` carries `status`, `code`,
210
+ `message`, `request_id` and the decoded `body`.
211
+
212
+ ```python
213
+ from ponk import PonkAPIError, PonkConflictError, PonkTransportError
214
+
215
+ try:
216
+ ponk.exit_agent(agent_id)
217
+ except PonkConflictError:
218
+ pass # an exit is already running; it will finish
219
+ except PonkTransportError:
220
+ # A fund call that times out has NOT necessarily failed: the work keeps
221
+ # running on the server. Exit is idempotent, so call it again and it
222
+ # continues rather than double-sending.
223
+ ponk.exit_agent(agent_id)
224
+ except PonkAPIError as e:
225
+ print(e.status, e.code, e.message, e.request_id)
226
+ ```
227
+
228
+ `compound`, `withdraw` and `exit` send several transactions and wait for each
229
+ confirmation, so they can take a minute. They get their own timeout,
230
+ `fund_timeout` (180s), separate from `timeout` (30s) for everything else.
231
+
232
+ ## Reading the numbers
233
+
234
+ * **A null USD figure means unknown, not zero.** An unpriceable pool returns
235
+ `null` rather than a fabricated 0. Render it as a dash. Treating it as zero
236
+ silently understates a position.
237
+ * **USD amounts on the agent and position endpoints are strings.** Feed them to
238
+ `decimal.Decimal` before doing arithmetic. They are strings precisely so a
239
+ float cannot round them.
240
+ * **`claimable_fees_usd` and `fee_pnl_usd` are different numbers.** The first is
241
+ the unclaimed fees sitting on the position right now, the second is lifetime
242
+ fees earned, claimed plus unclaimed. Do not add them.
243
+ * **PnL decomposes as Total = Price + Fee**: `pnl_usd`, `price_pnl_usd`,
244
+ `fee_pnl_usd`. `pnl_source` says whether the figures are Meteora's own
245
+ (`meteora_datapi`) or ponk's on-chain valuation (`oracle_estimate`).
246
+ * **`cost_basis_estimated`** marks a position ponk did not open. The basis is
247
+ then a first-observation stamp, so lifetime PnL stays `None` instead of
248
+ asserting a gain that contradicts your real numbers.
249
+ * **Every object keeps `raw_json`**, the exact decoded payload, so a field added
250
+ to the API after this client was written is still reachable.
251
+
252
+ ## Fees
253
+
254
+ Autonomous agents charge a performance fee on realized yield only. Principal is
255
+ never touched and no fee is taken on an unrealized gain. There is no charge for
256
+ a key, for a call, for creating an agent, for depositing or for withdrawing.
257
+
258
+ **An agent created through this API pays the API rate, not the app rate.** The
259
+ rate is fixed when the agent is created and follows that agent for its whole
260
+ life, including after you enable autonomous mode for it in the app. Read both
261
+ rates live rather than hardcoding them:
262
+
263
+ ```python
264
+ perks = ponk.ponk_perks(me.wallet_address)
265
+ print("app agents", perks.managed_agent_fee.effective_percent, "%")
266
+ print("api agents", perks.api_agent_fee.effective_percent, "%")
267
+ print("holding $PONK would make that", perks.api_agent_fee.holder_percent, "%")
268
+ ```
269
+
270
+ `agent.managed_fee_bps` is the rate that specific agent's skim actually
271
+ charges, before any $PONK holder discount.
272
+
273
+ ## Tests
274
+
275
+ Offline, no network, no fixtures to refresh:
276
+
277
+ ```bash
278
+ cd packages/sdk-python && python3 -m unittest discover -s tests -v
279
+ ```
@@ -0,0 +1,105 @@
1
+ """ponk - a thin Python client for the ponk public API.
2
+
3
+ from ponk import PonkClient
4
+
5
+ ponk = PonkClient(api_key="ponk_live_...")
6
+ print(ponk.whoami().wallet_address)
7
+
8
+ See `README.md` for the quickstart and `ponk.client.PonkClient` for the method
9
+ list. Standard library only.
10
+ """
11
+
12
+ from .client import (
13
+ DEFAULT_BASE_URL,
14
+ DEFAULT_FUND_TIMEOUT,
15
+ DEFAULT_TIMEOUT,
16
+ PonkClient,
17
+ )
18
+ from .webhooks import (
19
+ EVENT_KINDS,
20
+ InvalidSignature,
21
+ WebhookEvent,
22
+ parse_signature_header,
23
+ verify,
24
+ )
25
+ from .errors import (
26
+ PonkAPIError,
27
+ PonkAuthError,
28
+ PonkBadRequestError,
29
+ PonkConflictError,
30
+ PonkError,
31
+ PonkForbiddenError,
32
+ PonkNotFoundError,
33
+ PonkRateLimitedError,
34
+ PonkServerError,
35
+ PonkTransportError,
36
+ PonkUnprocessableError,
37
+ )
38
+ from .models import (
39
+ ActionLog,
40
+ ActionReceipt,
41
+ Agent,
42
+ AgentPerformance,
43
+ AgentPosition,
44
+ AgentWallet,
45
+ BinShare,
46
+ ClaimPayout,
47
+ ClaimPayoutToken,
48
+ ComponentStatus,
49
+ ExitSnapshot,
50
+ FeeRates,
51
+ Health,
52
+ HealthChecks,
53
+ PonkPerks,
54
+ PoolSnapshot,
55
+ Position,
56
+ TokenAmount,
57
+ WhoAmI,
58
+ Withdrawal,
59
+ )
60
+
61
+ __version__ = "0.2.0"
62
+
63
+ __all__ = [
64
+ "EVENT_KINDS",
65
+ "InvalidSignature",
66
+ "WebhookEvent",
67
+ "parse_signature_header",
68
+ "verify",
69
+ "__version__",
70
+ "DEFAULT_BASE_URL",
71
+ "DEFAULT_FUND_TIMEOUT",
72
+ "DEFAULT_TIMEOUT",
73
+ "PonkClient",
74
+ "PonkAPIError",
75
+ "PonkAuthError",
76
+ "PonkBadRequestError",
77
+ "PonkConflictError",
78
+ "PonkError",
79
+ "PonkForbiddenError",
80
+ "PonkNotFoundError",
81
+ "PonkRateLimitedError",
82
+ "PonkServerError",
83
+ "PonkTransportError",
84
+ "PonkUnprocessableError",
85
+ "ActionLog",
86
+ "ActionReceipt",
87
+ "Agent",
88
+ "AgentPerformance",
89
+ "AgentPosition",
90
+ "AgentWallet",
91
+ "BinShare",
92
+ "ClaimPayout",
93
+ "ClaimPayoutToken",
94
+ "ComponentStatus",
95
+ "ExitSnapshot",
96
+ "FeeRates",
97
+ "Health",
98
+ "HealthChecks",
99
+ "PonkPerks",
100
+ "PoolSnapshot",
101
+ "Position",
102
+ "TokenAmount",
103
+ "WhoAmI",
104
+ "Withdrawal",
105
+ ]