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 +300 -0
- ponk-0.2.0/README.md +279 -0
- ponk-0.2.0/ponk/__init__.py +105 -0
- ponk-0.2.0/ponk/client.py +374 -0
- ponk-0.2.0/ponk/errors.py +163 -0
- ponk-0.2.0/ponk/models.py +600 -0
- ponk-0.2.0/ponk/py.typed +0 -0
- ponk-0.2.0/ponk/webhooks.py +208 -0
- ponk-0.2.0/ponk.egg-info/PKG-INFO +300 -0
- ponk-0.2.0/ponk.egg-info/SOURCES.txt +14 -0
- ponk-0.2.0/ponk.egg-info/dependency_links.txt +1 -0
- ponk-0.2.0/ponk.egg-info/top_level.txt +1 -0
- ponk-0.2.0/pyproject.toml +45 -0
- ponk-0.2.0/setup.cfg +4 -0
- ponk-0.2.0/tests/test_client.py +317 -0
- ponk-0.2.0/tests/test_webhooks.py +126 -0
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
|
+
]
|