settld-api-sdk-python 0.1.0__tar.gz → 0.1.2__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.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: settld-api-sdk-python
3
+ Version: 0.1.2
4
+ Summary: Settld API SDK (Python)
5
+ Author: Settld
6
+ License: UNLICENSED
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Settld API SDK (Python)
11
+
12
+ Python client for Settld API endpoints, including high-level helpers:
13
+ - `first_verified_run` (register agents, run work, verify, settle)
14
+ - `first_paid_rfq` (rfq -> bid -> accept -> run -> settlement)
15
+ - run settlement/dispute lifecycle: `get_run_settlement_policy_replay`, `resolve_run_settlement`, `open_run_dispute`, `submit_run_dispute_evidence`, `escalate_run_dispute`, `close_run_dispute`
16
+ - `get_tenant_analytics` / `get_tenant_trust_graph`
17
+ - `list_tenant_trust_graph_snapshots` / `create_tenant_trust_graph_snapshot` / `diff_tenant_trust_graph`
18
+ - auth headers: `api_key` (Bearer) and optional `x_api_key` (Magic Link)
19
+
20
+ Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.
@@ -0,0 +1,11 @@
1
+ # Settld API SDK (Python)
2
+
3
+ Python client for Settld API endpoints, including high-level helpers:
4
+ - `first_verified_run` (register agents, run work, verify, settle)
5
+ - `first_paid_rfq` (rfq -> bid -> accept -> run -> settlement)
6
+ - run settlement/dispute lifecycle: `get_run_settlement_policy_replay`, `resolve_run_settlement`, `open_run_dispute`, `submit_run_dispute_evidence`, `escalate_run_dispute`, `close_run_dispute`
7
+ - `get_tenant_analytics` / `get_tenant_trust_graph`
8
+ - `list_tenant_trust_graph_snapshots` / `create_tenant_trust_graph_snapshot` / `diff_tenant_trust_graph`
9
+ - auth headers: `api_key` (Bearer) and optional `x_api_key` (Magic Link)
10
+
11
+ Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "settld-api-sdk-python"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Settld API SDK (Python)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,691 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import random
5
+ import time
6
+ import uuid
7
+ from typing import Any, Dict, Optional
8
+ from urllib import error, parse, request
9
+
10
+
11
+ def _assert_non_empty_string(value: Any, name: str) -> str:
12
+ if not isinstance(value, str) or value.strip() == "":
13
+ raise ValueError(f"{name} must be a non-empty string")
14
+ return value
15
+
16
+
17
+ def _random_request_id() -> str:
18
+ return f"req_{uuid.uuid4().hex}"
19
+
20
+
21
+ def _normalize_prefix(value: Optional[str], fallback: str) -> str:
22
+ if isinstance(value, str) and value.strip():
23
+ return value.strip()
24
+ return fallback
25
+
26
+
27
+ class SettldApiError(Exception):
28
+ def __init__(
29
+ self,
30
+ *,
31
+ status: int,
32
+ message: str,
33
+ code: Optional[str] = None,
34
+ details: Any = None,
35
+ request_id: Optional[str] = None,
36
+ ) -> None:
37
+ super().__init__(message)
38
+ self.status = status
39
+ self.code = code
40
+ self.details = details
41
+ self.request_id = request_id
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ return {
45
+ "status": self.status,
46
+ "code": self.code,
47
+ "message": str(self),
48
+ "details": self.details,
49
+ "requestId": self.request_id,
50
+ }
51
+
52
+
53
+ class SettldClient:
54
+ def __init__(
55
+ self,
56
+ *,
57
+ base_url: str,
58
+ tenant_id: str,
59
+ protocol: str = "1.0",
60
+ api_key: Optional[str] = None,
61
+ x_api_key: Optional[str] = None,
62
+ user_agent: Optional[str] = None,
63
+ timeout_seconds: float = 30.0,
64
+ ) -> None:
65
+ self.base_url = _assert_non_empty_string(base_url, "base_url").rstrip("/")
66
+ self.tenant_id = _assert_non_empty_string(tenant_id, "tenant_id")
67
+ self.protocol = protocol
68
+ self.api_key = api_key
69
+ self.x_api_key = x_api_key
70
+ self.user_agent = user_agent
71
+ self.timeout_seconds = timeout_seconds
72
+
73
+ def _request(
74
+ self,
75
+ method: str,
76
+ path: str,
77
+ *,
78
+ body: Optional[Dict[str, Any]] = None,
79
+ request_id: Optional[str] = None,
80
+ idempotency_key: Optional[str] = None,
81
+ expected_prev_chain_hash: Optional[str] = None,
82
+ timeout_seconds: Optional[float] = None,
83
+ ) -> Dict[str, Any]:
84
+ rid = request_id if request_id else _random_request_id()
85
+ headers = {
86
+ "content-type": "application/json",
87
+ "x-proxy-tenant-id": self.tenant_id,
88
+ "x-settld-protocol": self.protocol,
89
+ "x-request-id": rid,
90
+ }
91
+ if self.user_agent:
92
+ headers["user-agent"] = self.user_agent
93
+ if self.api_key:
94
+ headers["authorization"] = f"Bearer {self.api_key}"
95
+ if self.x_api_key:
96
+ headers["x-api-key"] = str(self.x_api_key)
97
+ if idempotency_key:
98
+ headers["x-idempotency-key"] = str(idempotency_key)
99
+ if expected_prev_chain_hash:
100
+ headers["x-proxy-expected-prev-chain-hash"] = str(expected_prev_chain_hash)
101
+
102
+ url = parse.urljoin(f"{self.base_url}/", path.lstrip("/"))
103
+ payload = None if body is None else json.dumps(body).encode("utf-8")
104
+ req = request.Request(url=url, data=payload, method=method, headers=headers)
105
+ timeout = self.timeout_seconds if timeout_seconds is None else timeout_seconds
106
+ try:
107
+ with request.urlopen(req, timeout=timeout) as response:
108
+ raw = response.read().decode("utf-8")
109
+ parsed = None
110
+ if raw:
111
+ try:
112
+ parsed = json.loads(raw)
113
+ except json.JSONDecodeError:
114
+ parsed = {"raw": raw}
115
+ response_headers = {str(k).lower(): str(v) for k, v in response.headers.items()}
116
+ return {
117
+ "ok": True,
118
+ "status": int(response.status),
119
+ "requestId": response_headers.get("x-request-id"),
120
+ "body": parsed,
121
+ "headers": response_headers,
122
+ }
123
+ except error.HTTPError as http_error:
124
+ raw = http_error.read().decode("utf-8")
125
+ parsed: Any = {}
126
+ if raw:
127
+ try:
128
+ parsed = json.loads(raw)
129
+ except json.JSONDecodeError:
130
+ parsed = {"raw": raw}
131
+ response_headers = {str(k).lower(): str(v) for k, v in http_error.headers.items()}
132
+ raise SettldApiError(
133
+ status=int(http_error.code),
134
+ code=parsed.get("code") if isinstance(parsed, dict) else None,
135
+ message=parsed.get("error", f"request failed ({http_error.code})") if isinstance(parsed, dict) else f"request failed ({http_error.code})",
136
+ details=parsed.get("details") if isinstance(parsed, dict) else None,
137
+ request_id=response_headers.get("x-request-id"),
138
+ ) from http_error
139
+
140
+ def register_agent(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
141
+ if not isinstance(body, dict):
142
+ raise ValueError("body is required")
143
+ _assert_non_empty_string(body.get("publicKeyPem"), "body.publicKeyPem")
144
+ return self._request("POST", "/agents/register", body=body, **opts)
145
+
146
+ def credit_agent_wallet(self, agent_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
147
+ _assert_non_empty_string(agent_id, "agent_id")
148
+ if not isinstance(body, dict):
149
+ raise ValueError("body is required")
150
+ return self._request("POST", f"/agents/{parse.quote(agent_id, safe='')}/wallet/credit", body=body, **opts)
151
+
152
+ def get_agent_wallet(self, agent_id: str, **opts: Any) -> Dict[str, Any]:
153
+ _assert_non_empty_string(agent_id, "agent_id")
154
+ return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/wallet", **opts)
155
+
156
+ def create_agent_run(self, agent_id: str, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
157
+ _assert_non_empty_string(agent_id, "agent_id")
158
+ run_body = {} if body is None else body
159
+ if not isinstance(run_body, dict):
160
+ raise ValueError("body must be an object")
161
+ return self._request("POST", f"/agents/{parse.quote(agent_id, safe='')}/runs", body=run_body, **opts)
162
+
163
+ def append_agent_run_event(
164
+ self,
165
+ agent_id: str,
166
+ run_id: str,
167
+ body: Dict[str, Any],
168
+ *,
169
+ expected_prev_chain_hash: str,
170
+ **opts: Any,
171
+ ) -> Dict[str, Any]:
172
+ _assert_non_empty_string(agent_id, "agent_id")
173
+ _assert_non_empty_string(run_id, "run_id")
174
+ _assert_non_empty_string(expected_prev_chain_hash, "expected_prev_chain_hash")
175
+ if not isinstance(body, dict):
176
+ raise ValueError("body is required")
177
+ _assert_non_empty_string(body.get("type"), "body.type")
178
+ return self._request(
179
+ "POST",
180
+ f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}/events",
181
+ body=body,
182
+ expected_prev_chain_hash=expected_prev_chain_hash,
183
+ **opts,
184
+ )
185
+
186
+ def get_agent_run(self, agent_id: str, run_id: str, **opts: Any) -> Dict[str, Any]:
187
+ _assert_non_empty_string(agent_id, "agent_id")
188
+ _assert_non_empty_string(run_id, "run_id")
189
+ return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}", **opts)
190
+
191
+ def list_agent_run_events(self, agent_id: str, run_id: str, **opts: Any) -> Dict[str, Any]:
192
+ _assert_non_empty_string(agent_id, "agent_id")
193
+ _assert_non_empty_string(run_id, "run_id")
194
+ return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}/events", **opts)
195
+
196
+ def get_run_verification(self, run_id: str, **opts: Any) -> Dict[str, Any]:
197
+ _assert_non_empty_string(run_id, "run_id")
198
+ return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/verification", **opts)
199
+
200
+ def get_run_settlement(self, run_id: str, **opts: Any) -> Dict[str, Any]:
201
+ _assert_non_empty_string(run_id, "run_id")
202
+ return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/settlement", **opts)
203
+
204
+ def get_run_settlement_policy_replay(self, run_id: str, **opts: Any) -> Dict[str, Any]:
205
+ _assert_non_empty_string(run_id, "run_id")
206
+ return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/settlement/policy-replay", **opts)
207
+
208
+ def resolve_run_settlement(self, run_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
209
+ _assert_non_empty_string(run_id, "run_id")
210
+ if not isinstance(body, dict):
211
+ raise ValueError("body is required")
212
+ return self._request("POST", f"/runs/{parse.quote(run_id, safe='')}/settlement/resolve", body=body, **opts)
213
+
214
+ def ops_lock_tool_call_hold(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
215
+ if not isinstance(body, dict):
216
+ raise ValueError("body is required")
217
+ return self._request("POST", "/ops/tool-calls/holds/lock", body=body, **opts)
218
+
219
+ def ops_list_tool_call_holds(self, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
220
+ params: Dict[str, Any] = {}
221
+ if isinstance(query, dict):
222
+ for key in ("agreementHash", "status", "limit", "offset"):
223
+ if query.get(key) is not None:
224
+ params[key] = query.get(key)
225
+ suffix = f"?{parse.urlencode(params)}" if params else ""
226
+ return self._request("GET", f"/ops/tool-calls/holds{suffix}", **opts)
227
+
228
+ def ops_get_tool_call_hold(self, hold_hash: str, **opts: Any) -> Dict[str, Any]:
229
+ _assert_non_empty_string(hold_hash, "hold_hash")
230
+ return self._request("GET", f"/ops/tool-calls/holds/{parse.quote(hold_hash, safe='')}", **opts)
231
+
232
+ def ops_run_tool_call_holdback_maintenance(self, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
233
+ payload = {} if body is None else body
234
+ if not isinstance(payload, dict):
235
+ raise ValueError("body must be an object")
236
+ return self._request("POST", "/ops/maintenance/tool-call-holdback/run", body=payload, **opts)
237
+
238
+ def tool_call_list_arbitration_cases(self, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
239
+ params: Dict[str, Any] = {}
240
+ if isinstance(query, dict):
241
+ for key in ("agreementHash", "status"):
242
+ if query.get(key) is not None:
243
+ params[key] = query.get(key)
244
+ suffix = f"?{parse.urlencode(params)}" if params else ""
245
+ return self._request("GET", f"/tool-calls/arbitration/cases{suffix}", **opts)
246
+
247
+ def tool_call_get_arbitration_case(self, case_id: str, **opts: Any) -> Dict[str, Any]:
248
+ _assert_non_empty_string(case_id, "case_id")
249
+ return self._request("GET", f"/tool-calls/arbitration/cases/{parse.quote(case_id, safe='')}", **opts)
250
+
251
+ def tool_call_open_arbitration(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
252
+ if not isinstance(body, dict):
253
+ raise ValueError("body is required")
254
+ return self._request("POST", "/tool-calls/arbitration/open", body=body, **opts)
255
+
256
+ def tool_call_submit_arbitration_verdict(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
257
+ if not isinstance(body, dict):
258
+ raise ValueError("body is required")
259
+ return self._request("POST", "/tool-calls/arbitration/verdict", body=body, **opts)
260
+
261
+ def ops_get_settlement_adjustment(self, adjustment_id: str, **opts: Any) -> Dict[str, Any]:
262
+ _assert_non_empty_string(adjustment_id, "adjustment_id")
263
+ return self._request("GET", f"/ops/settlement-adjustments/{parse.quote(adjustment_id, safe='')}", **opts)
264
+
265
+ def open_run_dispute(self, run_id: str, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
266
+ _assert_non_empty_string(run_id, "run_id")
267
+ payload = {} if body is None else body
268
+ if not isinstance(payload, dict):
269
+ raise ValueError("body must be an object")
270
+ return self._request("POST", f"/runs/{parse.quote(run_id, safe='')}/dispute/open", body=payload, **opts)
271
+
272
+ def close_run_dispute(self, run_id: str, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
273
+ _assert_non_empty_string(run_id, "run_id")
274
+ payload = {} if body is None else body
275
+ if not isinstance(payload, dict):
276
+ raise ValueError("body must be an object")
277
+ return self._request("POST", f"/runs/{parse.quote(run_id, safe='')}/dispute/close", body=payload, **opts)
278
+
279
+ def submit_run_dispute_evidence(self, run_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
280
+ _assert_non_empty_string(run_id, "run_id")
281
+ if not isinstance(body, dict):
282
+ raise ValueError("body is required")
283
+ _assert_non_empty_string(body.get("evidenceRef"), "body.evidenceRef")
284
+ return self._request("POST", f"/runs/{parse.quote(run_id, safe='')}/dispute/evidence", body=body, **opts)
285
+
286
+ def escalate_run_dispute(self, run_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
287
+ _assert_non_empty_string(run_id, "run_id")
288
+ if not isinstance(body, dict):
289
+ raise ValueError("body is required")
290
+ _assert_non_empty_string(body.get("escalationLevel"), "body.escalationLevel")
291
+ return self._request("POST", f"/runs/{parse.quote(run_id, safe='')}/dispute/escalate", body=body, **opts)
292
+
293
+ def create_marketplace_rfq(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
294
+ if not isinstance(body, dict):
295
+ raise ValueError("body is required")
296
+ return self._request("POST", "/marketplace/rfqs", body=body, **opts)
297
+
298
+ def list_marketplace_rfqs(self, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
299
+ params = {}
300
+ if isinstance(query, dict):
301
+ for key in ("status", "capability", "posterAgentId", "limit", "offset"):
302
+ if query.get(key) is not None:
303
+ params[key] = query.get(key)
304
+ suffix = f"?{parse.urlencode(params)}" if params else ""
305
+ return self._request("GET", f"/marketplace/rfqs{suffix}", **opts)
306
+
307
+ def submit_marketplace_bid(self, rfq_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
308
+ _assert_non_empty_string(rfq_id, "rfq_id")
309
+ if not isinstance(body, dict):
310
+ raise ValueError("body is required")
311
+ return self._request("POST", f"/marketplace/rfqs/{parse.quote(rfq_id, safe='')}/bids", body=body, **opts)
312
+
313
+ def list_marketplace_bids(self, rfq_id: str, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
314
+ _assert_non_empty_string(rfq_id, "rfq_id")
315
+ params = {}
316
+ if isinstance(query, dict):
317
+ for key in ("status", "bidderAgentId", "limit", "offset"):
318
+ if query.get(key) is not None:
319
+ params[key] = query.get(key)
320
+ suffix = f"?{parse.urlencode(params)}" if params else ""
321
+ return self._request("GET", f"/marketplace/rfqs/{parse.quote(rfq_id, safe='')}/bids{suffix}", **opts)
322
+
323
+ def accept_marketplace_bid(self, rfq_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
324
+ _assert_non_empty_string(rfq_id, "rfq_id")
325
+ if not isinstance(body, dict):
326
+ raise ValueError("body is required")
327
+ _assert_non_empty_string(body.get("bidId"), "body.bidId")
328
+ return self._request("POST", f"/marketplace/rfqs/{parse.quote(rfq_id, safe='')}/accept", body=body, **opts)
329
+
330
+ def get_tenant_analytics(self, tenant_id: str, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
331
+ _assert_non_empty_string(tenant_id, "tenant_id")
332
+ params = {}
333
+ if isinstance(query, dict):
334
+ for key in ("month", "bucket", "limit"):
335
+ if query.get(key) is not None:
336
+ params[key] = query.get(key)
337
+ suffix = f"?{parse.urlencode(params)}" if params else ""
338
+ return self._request("GET", f"/v1/tenants/{parse.quote(tenant_id, safe='')}/analytics{suffix}", **opts)
339
+
340
+ def get_tenant_trust_graph(self, tenant_id: str, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
341
+ _assert_non_empty_string(tenant_id, "tenant_id")
342
+ params = {}
343
+ if isinstance(query, dict):
344
+ for key in ("month", "minRuns", "maxEdges"):
345
+ if query.get(key) is not None:
346
+ params[key] = query.get(key)
347
+ suffix = f"?{parse.urlencode(params)}" if params else ""
348
+ return self._request("GET", f"/v1/tenants/{parse.quote(tenant_id, safe='')}/trust-graph{suffix}", **opts)
349
+
350
+ def list_tenant_trust_graph_snapshots(self, tenant_id: str, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
351
+ _assert_non_empty_string(tenant_id, "tenant_id")
352
+ params = {}
353
+ if isinstance(query, dict) and query.get("limit") is not None:
354
+ params["limit"] = query.get("limit")
355
+ suffix = f"?{parse.urlencode(params)}" if params else ""
356
+ return self._request("GET", f"/v1/tenants/{parse.quote(tenant_id, safe='')}/trust-graph/snapshots{suffix}", **opts)
357
+
358
+ def create_tenant_trust_graph_snapshot(self, tenant_id: str, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
359
+ _assert_non_empty_string(tenant_id, "tenant_id")
360
+ payload = {} if body is None else body
361
+ if not isinstance(payload, dict):
362
+ raise ValueError("body must be an object")
363
+ return self._request("POST", f"/v1/tenants/{parse.quote(tenant_id, safe='')}/trust-graph/snapshots", body=payload, **opts)
364
+
365
+ def diff_tenant_trust_graph(self, tenant_id: str, query: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
366
+ _assert_non_empty_string(tenant_id, "tenant_id")
367
+ params = {}
368
+ if isinstance(query, dict):
369
+ for key in ("baseMonth", "compareMonth", "limit", "minRuns", "maxEdges", "includeUnchanged"):
370
+ if query.get(key) is not None:
371
+ params[key] = query.get(key)
372
+ suffix = f"?{parse.urlencode(params)}" if params else ""
373
+ return self._request("GET", f"/v1/tenants/{parse.quote(tenant_id, safe='')}/trust-graph/diff{suffix}", **opts)
374
+
375
+ def first_paid_rfq(
376
+ self,
377
+ params: Dict[str, Any],
378
+ *,
379
+ idempotency_prefix: Optional[str] = None,
380
+ request_id_prefix: Optional[str] = None,
381
+ timeout_seconds: Optional[float] = None,
382
+ ) -> Dict[str, Any]:
383
+ if not isinstance(params, dict):
384
+ raise ValueError("params must be an object")
385
+
386
+ poster_agent = params.get("poster_agent")
387
+ bidder_agent = params.get("bidder_agent")
388
+ if not isinstance(poster_agent, dict):
389
+ raise ValueError("params.poster_agent is required")
390
+ if not isinstance(bidder_agent, dict):
391
+ raise ValueError("params.bidder_agent is required")
392
+ _assert_non_empty_string(poster_agent.get("publicKeyPem"), "params.poster_agent.publicKeyPem")
393
+ _assert_non_empty_string(bidder_agent.get("publicKeyPem"), "params.bidder_agent.publicKeyPem")
394
+
395
+ step_prefix = _normalize_prefix(
396
+ idempotency_prefix,
397
+ f"sdk_first_paid_rfq_{int(time.time() * 1000):x}_{random.randint(0, 0xFFFFFF):06x}",
398
+ )
399
+ request_prefix = _normalize_prefix(request_id_prefix, _random_request_id())
400
+
401
+ def make_step_opts(step: str, **extra: Any) -> Dict[str, Any]:
402
+ out = {
403
+ "request_id": f"{request_prefix}_{step}",
404
+ "idempotency_key": f"{step_prefix}_{step}",
405
+ "timeout_seconds": timeout_seconds,
406
+ }
407
+ out.update(extra)
408
+ return out
409
+
410
+ poster_registration = self.register_agent(poster_agent, **make_step_opts("register_poster"))
411
+ poster_agent_id = poster_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
412
+ _assert_non_empty_string(poster_agent_id, "poster_agent_id")
413
+
414
+ bidder_registration = self.register_agent(bidder_agent, **make_step_opts("register_bidder"))
415
+ bidder_agent_id = bidder_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
416
+ _assert_non_empty_string(bidder_agent_id, "bidder_agent_id")
417
+
418
+ accepted_by_registration = None
419
+ accepted_by_agent_id = poster_agent_id
420
+ accepted_by_agent = params.get("accepted_by_agent")
421
+ if accepted_by_agent is not None:
422
+ if not isinstance(accepted_by_agent, dict):
423
+ raise ValueError("params.accepted_by_agent must be an object")
424
+ _assert_non_empty_string(accepted_by_agent.get("publicKeyPem"), "params.accepted_by_agent.publicKeyPem")
425
+ accepted_by_registration = self.register_agent(accepted_by_agent, **make_step_opts("register_accepting_agent"))
426
+ accepted_by_agent_id = accepted_by_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
427
+ _assert_non_empty_string(accepted_by_agent_id, "accepted_by_agent_id")
428
+
429
+ payer_credit = params.get("payer_credit")
430
+ credit_result = None
431
+ if payer_credit is not None:
432
+ if not isinstance(payer_credit, dict):
433
+ raise ValueError("params.payer_credit must be an object")
434
+ amount_cents = payer_credit.get("amountCents")
435
+ if not isinstance(amount_cents, (int, float)) or amount_cents <= 0:
436
+ raise ValueError("params.payer_credit.amountCents must be a positive number")
437
+ credit_result = self.credit_agent_wallet(
438
+ poster_agent_id,
439
+ {
440
+ "amountCents": int(amount_cents),
441
+ "currency": payer_credit.get("currency", "USD"),
442
+ },
443
+ **make_step_opts("credit_poster_wallet"),
444
+ )
445
+
446
+ rfq_defaults = {
447
+ "rfqId": f"rfq_{step_prefix}",
448
+ "title": "SDK paid rfq",
449
+ "capability": "general",
450
+ "posterAgentId": poster_agent_id,
451
+ "budgetCents": 1000,
452
+ "currency": "USD",
453
+ }
454
+ rfq_body = {**rfq_defaults, **(params.get("rfq") if isinstance(params.get("rfq"), dict) else {})}
455
+ rfq_body["posterAgentId"] = poster_agent_id
456
+ create_rfq = self.create_marketplace_rfq(rfq_body, **make_step_opts("create_rfq"))
457
+ rfq = create_rfq.get("body", {}).get("rfq", {}) if isinstance(create_rfq.get("body"), dict) else {}
458
+ rfq_id = rfq.get("rfqId")
459
+ _assert_non_empty_string(rfq_id, "rfq_id")
460
+
461
+ bid_defaults = {
462
+ "bidId": f"bid_{step_prefix}",
463
+ "bidderAgentId": bidder_agent_id,
464
+ "amountCents": int(rfq_body.get("budgetCents", 1000)),
465
+ "currency": str(rfq_body.get("currency", "USD")),
466
+ "etaSeconds": 900,
467
+ }
468
+ bid_body = {**bid_defaults, **(params.get("bid") if isinstance(params.get("bid"), dict) else {})}
469
+ bid_body["bidderAgentId"] = bidder_agent_id
470
+ submit_bid = self.submit_marketplace_bid(rfq_id, bid_body, **make_step_opts("submit_bid"))
471
+ bid = submit_bid.get("body", {}).get("bid", {}) if isinstance(submit_bid.get("body"), dict) else {}
472
+ bid_id = bid.get("bidId")
473
+ _assert_non_empty_string(bid_id, "bid_id")
474
+
475
+ settlement_config = params.get("settlement") if isinstance(params.get("settlement"), dict) else {}
476
+ accept_defaults = {
477
+ "bidId": bid_id,
478
+ "acceptedByAgentId": accepted_by_agent_id,
479
+ "settlement": {
480
+ "payerAgentId": poster_agent_id,
481
+ "amountCents": int(bid_body.get("amountCents")),
482
+ "currency": str(bid_body.get("currency", rfq_body.get("currency", "USD"))),
483
+ },
484
+ }
485
+ accept_body = {**accept_defaults, **(params.get("accept") if isinstance(params.get("accept"), dict) else {})}
486
+ if not isinstance(accept_body.get("settlement"), dict):
487
+ accept_body["settlement"] = {}
488
+ accept_body["settlement"] = {**accept_defaults["settlement"], **accept_body["settlement"], **settlement_config}
489
+ accept_bid = self.accept_marketplace_bid(rfq_id, accept_body, **make_step_opts("accept_bid"))
490
+ accepted_body = accept_bid.get("body", {}) if isinstance(accept_bid.get("body"), dict) else {}
491
+ run = accepted_body.get("run", {}) if isinstance(accepted_body.get("run"), dict) else {}
492
+ run_id = run.get("runId")
493
+ _assert_non_empty_string(run_id, "run_id")
494
+
495
+ final_event = None
496
+ final_run = run
497
+ final_settlement = accepted_body.get("settlement")
498
+ if params.get("auto_complete", True):
499
+ prev_chain_hash = run.get("lastChainHash")
500
+ _assert_non_empty_string(prev_chain_hash, "run.lastChainHash")
501
+ completed_payload = dict(params.get("completed_payload") or {})
502
+ completed_payload.setdefault("outputRef", f"evidence://{run_id}/result.json")
503
+ if isinstance(params.get("completed_metrics"), dict):
504
+ completed_payload["metrics"] = params.get("completed_metrics")
505
+ elif "metrics" not in completed_payload:
506
+ completed_payload["metrics"] = {"settlementReleaseRatePct": 100}
507
+ completed = self.append_agent_run_event(
508
+ bidder_agent_id,
509
+ run_id,
510
+ {"type": "RUN_COMPLETED", "actor": {"type": "agent", "id": bidder_agent_id}, "payload": completed_payload},
511
+ expected_prev_chain_hash=prev_chain_hash,
512
+ **make_step_opts("run_completed"),
513
+ )
514
+ completed_body = completed.get("body", {}) if isinstance(completed.get("body"), dict) else {}
515
+ final_event = completed_body.get("event")
516
+ final_run = completed_body.get("run", final_run)
517
+ final_settlement = completed_body.get("settlement", final_settlement)
518
+
519
+ verification = self.get_run_verification(run_id, **make_step_opts("verification", idempotency_key=None))
520
+ settlement = self.get_run_settlement(run_id, **make_step_opts("settlement", idempotency_key=None))
521
+
522
+ return {
523
+ "ids": {
524
+ "poster_agent_id": poster_agent_id,
525
+ "bidder_agent_id": bidder_agent_id,
526
+ "accepted_by_agent_id": accepted_by_agent_id,
527
+ "rfq_id": rfq_id,
528
+ "bid_id": bid_id,
529
+ "run_id": run_id,
530
+ },
531
+ "poster_registration": poster_registration,
532
+ "bidder_registration": bidder_registration,
533
+ "accepted_by_registration": accepted_by_registration,
534
+ "payer_credit": credit_result,
535
+ "create_rfq": create_rfq,
536
+ "submit_bid": submit_bid,
537
+ "accept_bid": accept_bid,
538
+ "final_event": final_event,
539
+ "final_run": final_run,
540
+ "final_settlement": final_settlement,
541
+ "verification": verification,
542
+ "settlement": settlement,
543
+ }
544
+
545
+ def first_verified_run(
546
+ self,
547
+ params: Dict[str, Any],
548
+ *,
549
+ idempotency_prefix: Optional[str] = None,
550
+ request_id_prefix: Optional[str] = None,
551
+ timeout_seconds: Optional[float] = None,
552
+ ) -> Dict[str, Any]:
553
+ if not isinstance(params, dict):
554
+ raise ValueError("params must be an object")
555
+ payee_agent = params.get("payee_agent")
556
+ if not isinstance(payee_agent, dict):
557
+ raise ValueError("params.payee_agent is required")
558
+ _assert_non_empty_string(payee_agent.get("publicKeyPem"), "params.payee_agent.publicKeyPem")
559
+
560
+ step_prefix = _normalize_prefix(
561
+ idempotency_prefix,
562
+ f"sdk_first_verified_run_{int(time.time() * 1000):x}_{random.randint(0, 0xFFFFFF):06x}",
563
+ )
564
+ request_prefix = _normalize_prefix(request_id_prefix, _random_request_id())
565
+
566
+ def make_step_opts(step: str, **extra: Any) -> Dict[str, Any]:
567
+ out = {
568
+ "request_id": f"{request_prefix}_{step}",
569
+ "idempotency_key": f"{step_prefix}_{step}",
570
+ "timeout_seconds": timeout_seconds,
571
+ }
572
+ out.update(extra)
573
+ return out
574
+
575
+ payee_registration = self.register_agent(payee_agent, **make_step_opts("register_payee"))
576
+ payee_agent_id = payee_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
577
+ _assert_non_empty_string(payee_agent_id, "payee_agent_id")
578
+
579
+ payer_registration = None
580
+ payer_credit = None
581
+ payer_agent_id = None
582
+ payer_agent = params.get("payer_agent")
583
+ if payer_agent is not None:
584
+ if not isinstance(payer_agent, dict):
585
+ raise ValueError("params.payer_agent must be an object")
586
+ _assert_non_empty_string(payer_agent.get("publicKeyPem"), "params.payer_agent.publicKeyPem")
587
+ payer_registration = self.register_agent(payer_agent, **make_step_opts("register_payer"))
588
+ payer_agent_id = payer_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
589
+ _assert_non_empty_string(payer_agent_id, "payer_agent_id")
590
+
591
+ settlement = params.get("settlement") if isinstance(params.get("settlement"), dict) else None
592
+ settlement_amount_cents = settlement.get("amountCents") if settlement else None
593
+ settlement_currency = settlement.get("currency", "USD") if settlement else "USD"
594
+ settlement_payer_agent_id = (
595
+ settlement.get("payerAgentId")
596
+ if settlement and isinstance(settlement.get("payerAgentId"), str)
597
+ else payer_agent_id
598
+ )
599
+ if settlement_amount_cents is not None and settlement_payer_agent_id is None:
600
+ raise ValueError("params.payer_agent or params.settlement.payerAgentId is required when settlement is requested")
601
+
602
+ payer_credit_input = params.get("payer_credit")
603
+ if payer_credit_input is not None:
604
+ if not isinstance(payer_credit_input, dict):
605
+ raise ValueError("params.payer_credit must be an object")
606
+ payer_credit_amount = payer_credit_input.get("amountCents")
607
+ if not isinstance(payer_credit_amount, (int, float)) or payer_credit_amount <= 0:
608
+ raise ValueError("params.payer_credit.amountCents must be a positive number")
609
+ if not payer_agent_id:
610
+ raise ValueError("params.payer_agent is required when params.payer_credit is provided")
611
+ payer_credit = self.credit_agent_wallet(
612
+ payer_agent_id,
613
+ {
614
+ "amountCents": int(payer_credit_amount),
615
+ "currency": payer_credit_input.get("currency", settlement_currency),
616
+ },
617
+ **make_step_opts("credit_payer_wallet"),
618
+ )
619
+
620
+ run_body = dict(params.get("run") or {})
621
+ if settlement_amount_cents is not None:
622
+ if not isinstance(settlement_amount_cents, (int, float)) or settlement_amount_cents <= 0:
623
+ raise ValueError("params.settlement.amountCents must be a positive number")
624
+ run_body["settlement"] = {
625
+ "payerAgentId": settlement_payer_agent_id,
626
+ "amountCents": int(settlement_amount_cents),
627
+ "currency": settlement_currency,
628
+ }
629
+
630
+ run_created = self.create_agent_run(payee_agent_id, run_body, **make_step_opts("create_run"))
631
+ run_id = run_created.get("body", {}).get("run", {}).get("runId")
632
+ _assert_non_empty_string(run_id, "run_id")
633
+ prev_chain_hash = run_created.get("body", {}).get("run", {}).get("lastChainHash")
634
+ _assert_non_empty_string(prev_chain_hash, "run_created.body.run.lastChainHash")
635
+
636
+ actor = params.get("actor") or {"type": "agent", "id": payee_agent_id}
637
+ started_payload = params.get("started_payload") or {"startedBy": "sdk.first_verified_run"}
638
+ run_started = self.append_agent_run_event(
639
+ payee_agent_id,
640
+ run_id,
641
+ {"type": "RUN_STARTED", "actor": actor, "payload": started_payload},
642
+ expected_prev_chain_hash=prev_chain_hash,
643
+ **make_step_opts("run_started"),
644
+ )
645
+ prev_chain_hash = run_started.get("body", {}).get("run", {}).get("lastChainHash")
646
+ _assert_non_empty_string(prev_chain_hash, "run_started.body.run.lastChainHash")
647
+
648
+ evidence_ref = params.get("evidence_ref") if isinstance(params.get("evidence_ref"), str) and params.get("evidence_ref").strip() else f"evidence://{run_id}/output.json"
649
+ evidence_payload = params.get("evidence_payload") if isinstance(params.get("evidence_payload"), dict) else {"evidenceRef": evidence_ref}
650
+ run_evidence_added = self.append_agent_run_event(
651
+ payee_agent_id,
652
+ run_id,
653
+ {"type": "EVIDENCE_ADDED", "actor": actor, "payload": evidence_payload},
654
+ expected_prev_chain_hash=prev_chain_hash,
655
+ **make_step_opts("evidence_added"),
656
+ )
657
+ prev_chain_hash = run_evidence_added.get("body", {}).get("run", {}).get("lastChainHash")
658
+ _assert_non_empty_string(prev_chain_hash, "run_evidence_added.body.run.lastChainHash")
659
+
660
+ completed_payload = dict(params.get("completed_payload") or {})
661
+ output_ref = params.get("output_ref") if isinstance(params.get("output_ref"), str) and params.get("output_ref").strip() else evidence_ref
662
+ completed_payload["outputRef"] = output_ref
663
+ if isinstance(params.get("completed_metrics"), dict):
664
+ completed_payload["metrics"] = params.get("completed_metrics")
665
+ run_completed = self.append_agent_run_event(
666
+ payee_agent_id,
667
+ run_id,
668
+ {"type": "RUN_COMPLETED", "actor": actor, "payload": completed_payload},
669
+ expected_prev_chain_hash=prev_chain_hash,
670
+ **make_step_opts("run_completed"),
671
+ )
672
+
673
+ run = self.get_agent_run(payee_agent_id, run_id, **make_step_opts("get_run"))
674
+ verification = self.get_run_verification(run_id, **make_step_opts("get_verification"))
675
+ settlement_out = None
676
+ if run_body.get("settlement") or run_created.get("body", {}).get("settlement") or run_completed.get("body", {}).get("settlement"):
677
+ settlement_out = self.get_run_settlement(run_id, **make_step_opts("get_settlement"))
678
+
679
+ return {
680
+ "ids": {"run_id": run_id, "payee_agent_id": payee_agent_id, "payer_agent_id": payer_agent_id},
681
+ "payee_registration": payee_registration,
682
+ "payer_registration": payer_registration,
683
+ "payer_credit": payer_credit,
684
+ "run_created": run_created,
685
+ "run_started": run_started,
686
+ "run_evidence_added": run_evidence_added,
687
+ "run_completed": run_completed,
688
+ "run": run,
689
+ "verification": verification,
690
+ "settlement": settlement_out,
691
+ }
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: settld-api-sdk-python
3
+ Version: 0.1.2
4
+ Summary: Settld API SDK (Python)
5
+ Author: Settld
6
+ License: UNLICENSED
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Settld API SDK (Python)
11
+
12
+ Python client for Settld API endpoints, including high-level helpers:
13
+ - `first_verified_run` (register agents, run work, verify, settle)
14
+ - `first_paid_rfq` (rfq -> bid -> accept -> run -> settlement)
15
+ - run settlement/dispute lifecycle: `get_run_settlement_policy_replay`, `resolve_run_settlement`, `open_run_dispute`, `submit_run_dispute_evidence`, `escalate_run_dispute`, `close_run_dispute`
16
+ - `get_tenant_analytics` / `get_tenant_trust_graph`
17
+ - `list_tenant_trust_graph_snapshots` / `create_tenant_trust_graph_snapshot` / `diff_tenant_trust_graph`
18
+ - auth headers: `api_key` (Bearer) and optional `x_api_key` (Magic Link)
19
+
20
+ Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: settld-api-sdk-python
3
- Version: 0.1.0
4
- Summary: Settld API SDK (Python)
5
- Author: Settld
6
- License: UNLICENSED
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
9
-
10
- # Settld API SDK (Python)
11
-
12
- Python client for Settld API endpoints, including a high-level `first_verified_run` helper.
13
-
14
- Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.
@@ -1,5 +0,0 @@
1
- # Settld API SDK (Python)
2
-
3
- Python client for Settld API endpoints, including a high-level `first_verified_run` helper.
4
-
5
- Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.
@@ -1,346 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import random
5
- import time
6
- import uuid
7
- from typing import Any, Dict, Optional
8
- from urllib import error, parse, request
9
-
10
-
11
- def _assert_non_empty_string(value: Any, name: str) -> str:
12
- if not isinstance(value, str) or value.strip() == "":
13
- raise ValueError(f"{name} must be a non-empty string")
14
- return value
15
-
16
-
17
- def _random_request_id() -> str:
18
- return f"req_{uuid.uuid4().hex}"
19
-
20
-
21
- def _normalize_prefix(value: Optional[str], fallback: str) -> str:
22
- if isinstance(value, str) and value.strip():
23
- return value.strip()
24
- return fallback
25
-
26
-
27
- class SettldApiError(Exception):
28
- def __init__(
29
- self,
30
- *,
31
- status: int,
32
- message: str,
33
- code: Optional[str] = None,
34
- details: Any = None,
35
- request_id: Optional[str] = None,
36
- ) -> None:
37
- super().__init__(message)
38
- self.status = status
39
- self.code = code
40
- self.details = details
41
- self.request_id = request_id
42
-
43
- def to_dict(self) -> Dict[str, Any]:
44
- return {
45
- "status": self.status,
46
- "code": self.code,
47
- "message": str(self),
48
- "details": self.details,
49
- "requestId": self.request_id,
50
- }
51
-
52
-
53
- class SettldClient:
54
- def __init__(
55
- self,
56
- *,
57
- base_url: str,
58
- tenant_id: str,
59
- protocol: str = "1.0",
60
- api_key: Optional[str] = None,
61
- user_agent: Optional[str] = None,
62
- timeout_seconds: float = 30.0,
63
- ) -> None:
64
- self.base_url = _assert_non_empty_string(base_url, "base_url").rstrip("/")
65
- self.tenant_id = _assert_non_empty_string(tenant_id, "tenant_id")
66
- self.protocol = protocol
67
- self.api_key = api_key
68
- self.user_agent = user_agent
69
- self.timeout_seconds = timeout_seconds
70
-
71
- def _request(
72
- self,
73
- method: str,
74
- path: str,
75
- *,
76
- body: Optional[Dict[str, Any]] = None,
77
- request_id: Optional[str] = None,
78
- idempotency_key: Optional[str] = None,
79
- expected_prev_chain_hash: Optional[str] = None,
80
- timeout_seconds: Optional[float] = None,
81
- ) -> Dict[str, Any]:
82
- rid = request_id if request_id else _random_request_id()
83
- headers = {
84
- "content-type": "application/json",
85
- "x-proxy-tenant-id": self.tenant_id,
86
- "x-settld-protocol": self.protocol,
87
- "x-request-id": rid,
88
- }
89
- if self.user_agent:
90
- headers["user-agent"] = self.user_agent
91
- if self.api_key:
92
- headers["authorization"] = f"Bearer {self.api_key}"
93
- if idempotency_key:
94
- headers["x-idempotency-key"] = str(idempotency_key)
95
- if expected_prev_chain_hash:
96
- headers["x-proxy-expected-prev-chain-hash"] = str(expected_prev_chain_hash)
97
-
98
- url = parse.urljoin(f"{self.base_url}/", path.lstrip("/"))
99
- payload = None if body is None else json.dumps(body).encode("utf-8")
100
- req = request.Request(url=url, data=payload, method=method, headers=headers)
101
- timeout = self.timeout_seconds if timeout_seconds is None else timeout_seconds
102
- try:
103
- with request.urlopen(req, timeout=timeout) as response:
104
- raw = response.read().decode("utf-8")
105
- parsed = None
106
- if raw:
107
- try:
108
- parsed = json.loads(raw)
109
- except json.JSONDecodeError:
110
- parsed = {"raw": raw}
111
- response_headers = {str(k).lower(): str(v) for k, v in response.headers.items()}
112
- return {
113
- "ok": True,
114
- "status": int(response.status),
115
- "requestId": response_headers.get("x-request-id"),
116
- "body": parsed,
117
- "headers": response_headers,
118
- }
119
- except error.HTTPError as http_error:
120
- raw = http_error.read().decode("utf-8")
121
- parsed: Any = {}
122
- if raw:
123
- try:
124
- parsed = json.loads(raw)
125
- except json.JSONDecodeError:
126
- parsed = {"raw": raw}
127
- response_headers = {str(k).lower(): str(v) for k, v in http_error.headers.items()}
128
- raise SettldApiError(
129
- status=int(http_error.code),
130
- code=parsed.get("code") if isinstance(parsed, dict) else None,
131
- message=parsed.get("error", f"request failed ({http_error.code})") if isinstance(parsed, dict) else f"request failed ({http_error.code})",
132
- details=parsed.get("details") if isinstance(parsed, dict) else None,
133
- request_id=response_headers.get("x-request-id"),
134
- ) from http_error
135
-
136
- def register_agent(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
137
- if not isinstance(body, dict):
138
- raise ValueError("body is required")
139
- _assert_non_empty_string(body.get("publicKeyPem"), "body.publicKeyPem")
140
- return self._request("POST", "/agents/register", body=body, **opts)
141
-
142
- def credit_agent_wallet(self, agent_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
143
- _assert_non_empty_string(agent_id, "agent_id")
144
- if not isinstance(body, dict):
145
- raise ValueError("body is required")
146
- return self._request("POST", f"/agents/{parse.quote(agent_id, safe='')}/wallet/credit", body=body, **opts)
147
-
148
- def get_agent_wallet(self, agent_id: str, **opts: Any) -> Dict[str, Any]:
149
- _assert_non_empty_string(agent_id, "agent_id")
150
- return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/wallet", **opts)
151
-
152
- def create_agent_run(self, agent_id: str, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
153
- _assert_non_empty_string(agent_id, "agent_id")
154
- run_body = {} if body is None else body
155
- if not isinstance(run_body, dict):
156
- raise ValueError("body must be an object")
157
- return self._request("POST", f"/agents/{parse.quote(agent_id, safe='')}/runs", body=run_body, **opts)
158
-
159
- def append_agent_run_event(
160
- self,
161
- agent_id: str,
162
- run_id: str,
163
- body: Dict[str, Any],
164
- *,
165
- expected_prev_chain_hash: str,
166
- **opts: Any,
167
- ) -> Dict[str, Any]:
168
- _assert_non_empty_string(agent_id, "agent_id")
169
- _assert_non_empty_string(run_id, "run_id")
170
- _assert_non_empty_string(expected_prev_chain_hash, "expected_prev_chain_hash")
171
- if not isinstance(body, dict):
172
- raise ValueError("body is required")
173
- _assert_non_empty_string(body.get("type"), "body.type")
174
- return self._request(
175
- "POST",
176
- f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}/events",
177
- body=body,
178
- expected_prev_chain_hash=expected_prev_chain_hash,
179
- **opts,
180
- )
181
-
182
- def get_agent_run(self, agent_id: str, run_id: str, **opts: Any) -> Dict[str, Any]:
183
- _assert_non_empty_string(agent_id, "agent_id")
184
- _assert_non_empty_string(run_id, "run_id")
185
- return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}", **opts)
186
-
187
- def list_agent_run_events(self, agent_id: str, run_id: str, **opts: Any) -> Dict[str, Any]:
188
- _assert_non_empty_string(agent_id, "agent_id")
189
- _assert_non_empty_string(run_id, "run_id")
190
- return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}/events", **opts)
191
-
192
- def get_run_verification(self, run_id: str, **opts: Any) -> Dict[str, Any]:
193
- _assert_non_empty_string(run_id, "run_id")
194
- return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/verification", **opts)
195
-
196
- def get_run_settlement(self, run_id: str, **opts: Any) -> Dict[str, Any]:
197
- _assert_non_empty_string(run_id, "run_id")
198
- return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/settlement", **opts)
199
-
200
- def first_verified_run(
201
- self,
202
- params: Dict[str, Any],
203
- *,
204
- idempotency_prefix: Optional[str] = None,
205
- request_id_prefix: Optional[str] = None,
206
- timeout_seconds: Optional[float] = None,
207
- ) -> Dict[str, Any]:
208
- if not isinstance(params, dict):
209
- raise ValueError("params must be an object")
210
- payee_agent = params.get("payee_agent")
211
- if not isinstance(payee_agent, dict):
212
- raise ValueError("params.payee_agent is required")
213
- _assert_non_empty_string(payee_agent.get("publicKeyPem"), "params.payee_agent.publicKeyPem")
214
-
215
- step_prefix = _normalize_prefix(
216
- idempotency_prefix,
217
- f"sdk_first_verified_run_{int(time.time() * 1000):x}_{random.randint(0, 0xFFFFFF):06x}",
218
- )
219
- request_prefix = _normalize_prefix(request_id_prefix, _random_request_id())
220
-
221
- def make_step_opts(step: str, **extra: Any) -> Dict[str, Any]:
222
- out = {
223
- "request_id": f"{request_prefix}_{step}",
224
- "idempotency_key": f"{step_prefix}_{step}",
225
- "timeout_seconds": timeout_seconds,
226
- }
227
- out.update(extra)
228
- return out
229
-
230
- payee_registration = self.register_agent(payee_agent, **make_step_opts("register_payee"))
231
- payee_agent_id = payee_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
232
- _assert_non_empty_string(payee_agent_id, "payee_agent_id")
233
-
234
- payer_registration = None
235
- payer_credit = None
236
- payer_agent_id = None
237
- payer_agent = params.get("payer_agent")
238
- if payer_agent is not None:
239
- if not isinstance(payer_agent, dict):
240
- raise ValueError("params.payer_agent must be an object")
241
- _assert_non_empty_string(payer_agent.get("publicKeyPem"), "params.payer_agent.publicKeyPem")
242
- payer_registration = self.register_agent(payer_agent, **make_step_opts("register_payer"))
243
- payer_agent_id = payer_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
244
- _assert_non_empty_string(payer_agent_id, "payer_agent_id")
245
-
246
- settlement = params.get("settlement") if isinstance(params.get("settlement"), dict) else None
247
- settlement_amount_cents = settlement.get("amountCents") if settlement else None
248
- settlement_currency = settlement.get("currency", "USD") if settlement else "USD"
249
- settlement_payer_agent_id = (
250
- settlement.get("payerAgentId")
251
- if settlement and isinstance(settlement.get("payerAgentId"), str)
252
- else payer_agent_id
253
- )
254
- if settlement_amount_cents is not None and settlement_payer_agent_id is None:
255
- raise ValueError("params.payer_agent or params.settlement.payerAgentId is required when settlement is requested")
256
-
257
- payer_credit_input = params.get("payer_credit")
258
- if payer_credit_input is not None:
259
- if not isinstance(payer_credit_input, dict):
260
- raise ValueError("params.payer_credit must be an object")
261
- payer_credit_amount = payer_credit_input.get("amountCents")
262
- if not isinstance(payer_credit_amount, (int, float)) or payer_credit_amount <= 0:
263
- raise ValueError("params.payer_credit.amountCents must be a positive number")
264
- if not payer_agent_id:
265
- raise ValueError("params.payer_agent is required when params.payer_credit is provided")
266
- payer_credit = self.credit_agent_wallet(
267
- payer_agent_id,
268
- {
269
- "amountCents": int(payer_credit_amount),
270
- "currency": payer_credit_input.get("currency", settlement_currency),
271
- },
272
- **make_step_opts("credit_payer_wallet"),
273
- )
274
-
275
- run_body = dict(params.get("run") or {})
276
- if settlement_amount_cents is not None:
277
- if not isinstance(settlement_amount_cents, (int, float)) or settlement_amount_cents <= 0:
278
- raise ValueError("params.settlement.amountCents must be a positive number")
279
- run_body["settlement"] = {
280
- "payerAgentId": settlement_payer_agent_id,
281
- "amountCents": int(settlement_amount_cents),
282
- "currency": settlement_currency,
283
- }
284
-
285
- run_created = self.create_agent_run(payee_agent_id, run_body, **make_step_opts("create_run"))
286
- run_id = run_created.get("body", {}).get("run", {}).get("runId")
287
- _assert_non_empty_string(run_id, "run_id")
288
- prev_chain_hash = run_created.get("body", {}).get("run", {}).get("lastChainHash")
289
- _assert_non_empty_string(prev_chain_hash, "run_created.body.run.lastChainHash")
290
-
291
- actor = params.get("actor") or {"type": "agent", "id": payee_agent_id}
292
- started_payload = params.get("started_payload") or {"startedBy": "sdk.first_verified_run"}
293
- run_started = self.append_agent_run_event(
294
- payee_agent_id,
295
- run_id,
296
- {"type": "RUN_STARTED", "actor": actor, "payload": started_payload},
297
- expected_prev_chain_hash=prev_chain_hash,
298
- **make_step_opts("run_started"),
299
- )
300
- prev_chain_hash = run_started.get("body", {}).get("run", {}).get("lastChainHash")
301
- _assert_non_empty_string(prev_chain_hash, "run_started.body.run.lastChainHash")
302
-
303
- evidence_ref = params.get("evidence_ref") if isinstance(params.get("evidence_ref"), str) and params.get("evidence_ref").strip() else f"evidence://{run_id}/output.json"
304
- evidence_payload = params.get("evidence_payload") if isinstance(params.get("evidence_payload"), dict) else {"evidenceRef": evidence_ref}
305
- run_evidence_added = self.append_agent_run_event(
306
- payee_agent_id,
307
- run_id,
308
- {"type": "EVIDENCE_ADDED", "actor": actor, "payload": evidence_payload},
309
- expected_prev_chain_hash=prev_chain_hash,
310
- **make_step_opts("evidence_added"),
311
- )
312
- prev_chain_hash = run_evidence_added.get("body", {}).get("run", {}).get("lastChainHash")
313
- _assert_non_empty_string(prev_chain_hash, "run_evidence_added.body.run.lastChainHash")
314
-
315
- completed_payload = dict(params.get("completed_payload") or {})
316
- output_ref = params.get("output_ref") if isinstance(params.get("output_ref"), str) and params.get("output_ref").strip() else evidence_ref
317
- completed_payload["outputRef"] = output_ref
318
- if isinstance(params.get("completed_metrics"), dict):
319
- completed_payload["metrics"] = params.get("completed_metrics")
320
- run_completed = self.append_agent_run_event(
321
- payee_agent_id,
322
- run_id,
323
- {"type": "RUN_COMPLETED", "actor": actor, "payload": completed_payload},
324
- expected_prev_chain_hash=prev_chain_hash,
325
- **make_step_opts("run_completed"),
326
- )
327
-
328
- run = self.get_agent_run(payee_agent_id, run_id, **make_step_opts("get_run"))
329
- verification = self.get_run_verification(run_id, **make_step_opts("get_verification"))
330
- settlement_out = None
331
- if run_body.get("settlement") or run_created.get("body", {}).get("settlement") or run_completed.get("body", {}).get("settlement"):
332
- settlement_out = self.get_run_settlement(run_id, **make_step_opts("get_settlement"))
333
-
334
- return {
335
- "ids": {"run_id": run_id, "payee_agent_id": payee_agent_id, "payer_agent_id": payer_agent_id},
336
- "payee_registration": payee_registration,
337
- "payer_registration": payer_registration,
338
- "payer_credit": payer_credit,
339
- "run_created": run_created,
340
- "run_started": run_started,
341
- "run_evidence_added": run_evidence_added,
342
- "run_completed": run_completed,
343
- "run": run,
344
- "verification": verification,
345
- "settlement": settlement_out,
346
- }
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: settld-api-sdk-python
3
- Version: 0.1.0
4
- Summary: Settld API SDK (Python)
5
- Author: Settld
6
- License: UNLICENSED
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
9
-
10
- # Settld API SDK (Python)
11
-
12
- Python client for Settld API endpoints, including a high-level `first_verified_run` helper.
13
-
14
- Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.