@zeroxyz/sdk 0.1.0
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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/chunk-7STN754W.cjs +3048 -0
- package/dist/chunk-7STN754W.cjs.map +1 -0
- package/dist/chunk-XQEHJX2X.js +3017 -0
- package/dist/chunk-XQEHJX2X.js.map +1 -0
- package/dist/client-B50UYxPr.d.cts +836 -0
- package/dist/client-B50UYxPr.d.ts +836 -0
- package/dist/index.cjs +150 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +51 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +14 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +47 -0
- package/dist/testing.js.map +1 -0
- package/package.json +95 -0
- package/testing/package.json +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@zeroxyz/sdk` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with the pre-1.0 caveat that minor versions may include breaking changes.
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
Initial public release.
|
|
10
|
+
|
|
11
|
+
- `ZeroClient` with namespaces: `auth`, `auth.device`, `bugReports`, `capabilities`, `payments`, `runs`, `wallet`.
|
|
12
|
+
- `client.fetch()` — one-call paid HTTP that probes, pays (x402 or MPP), and records the run.
|
|
13
|
+
- `client.search()` — capability discovery.
|
|
14
|
+
- `client.withAccount()` — cached per-account sub-clients for multi-tenant servers.
|
|
15
|
+
- Auth modes: BYO `viem` `LocalAccount` (account mode), forwarded user session (session mode with refresh-token rotation), or unauthenticated.
|
|
16
|
+
- `@zeroxyz/sdk/testing` — light mock helpers (`createTestClient`, `createMockFetch`, `respondJson`). No runtime peers.
|
|
17
|
+
- Structured errors: `ZeroApiError`, `ZeroAuthError`, `ZeroPaymentError`, `ZeroSessionCloseFailedError`, `ZeroTimeoutError`, `ZeroConfigurationError`, `ZeroWalletError`, `ZeroValidationError`.
|
|
18
|
+
- Dual ESM + CJS build with type declarations.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pie Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# @zeroxyz/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [Zero](https://zero.xyz) — the search and payment layer for AI agents.
|
|
4
|
+
|
|
5
|
+
Zero indexes the API capabilities published across the web and gives an agent one interface to find them, call them, and pay for them. You search for a capability, call its URL with `client.fetch()`, and the SDK settles payment automatically — over x402 or MPP, in USDC — handing you back the response and the payment receipt together. The wallet is the agent's identity, so there are no per-service API keys to provision.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @zeroxyz/sdk
|
|
11
|
+
# npm install @zeroxyz/sdk
|
|
12
|
+
# yarn add @zeroxyz/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires **Node 20.3+**.
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
Search needs no credentials. This is the smallest thing that runs:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { ZeroClient } from "@zeroxyz/sdk";
|
|
23
|
+
|
|
24
|
+
const client = new ZeroClient();
|
|
25
|
+
|
|
26
|
+
const { capabilities } = await client.search("current weather in tokyo");
|
|
27
|
+
for (const cap of capabilities) {
|
|
28
|
+
console.log(cap.id, cap.name, cap.url, cap.availabilityStatus);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Calling and paying for a capability needs a wallet. That's the next section.
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
A client runs in one of three modes. Pick one at construction:
|
|
37
|
+
|
|
38
|
+
| Mode | You provide | Use it when |
|
|
39
|
+
| ----------- | ---------------------------------------- | --------------------------------------------------------------------------- |
|
|
40
|
+
| **Account** | A `viem` `LocalAccount` (your wallet key)| You hold the wallet and sign locally. The default for partners and backends.|
|
|
41
|
+
| **Session** | A Zero-issued access + refresh token | You act on behalf of a Zero user — e.g. forwarding a session your server holds. |
|
|
42
|
+
| **None** | — | Public routes only (`search`, `capabilities.get`). |
|
|
43
|
+
|
|
44
|
+
Passing both `account` and `session` is a configuration error — the two modes don't compose.
|
|
45
|
+
|
|
46
|
+
### Account mode (bring your own key)
|
|
47
|
+
|
|
48
|
+
The common case: you have a private key and sign requests locally.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { ZeroClient } from "@zeroxyz/sdk";
|
|
52
|
+
|
|
53
|
+
const client = ZeroClient.fromPrivateKey(
|
|
54
|
+
process.env.ZERO_PRIVATE_KEY as `0x${string}`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const result = await client.fetch("https://example.com/weather?city=tokyo", {
|
|
58
|
+
maxPay: "0.01", // refuse to pay more than $0.01 USDC for this call
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(result.outcome); // "success" | "payment_failed" | ...
|
|
62
|
+
console.log(result.body); // parsed JSON when the response is JSON
|
|
63
|
+
console.log(result.payment); // { protocol, txHash, ... }, or null on a free call
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`fromPrivateKey` and `fromMnemonic` are convenience constructors. For any other signer — a KMS-backed account, a hardware wallet, a custom `viem` account — build the account yourself and pass it in:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
70
|
+
|
|
71
|
+
const account = privateKeyToAccount(process.env.ZERO_PRIVATE_KEY as `0x${string}`);
|
|
72
|
+
const client = new ZeroClient({ account });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Session mode (acting for a Zero user)
|
|
76
|
+
|
|
77
|
+
Use this when your server holds a Zero-issued token pair for a user and wants the SDK to sign with that user's Zero-managed wallet, resolved on the first paid call:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const client = new ZeroClient({
|
|
81
|
+
session: {
|
|
82
|
+
accessToken: userSession.access,
|
|
83
|
+
refreshToken: userSession.refresh,
|
|
84
|
+
onRefreshed: async ({ accessToken, refreshToken }) => {
|
|
85
|
+
await db.updateUserSession(userId, { accessToken, refreshToken });
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The SDK refreshes the access token automatically after a `401`, and every refresh rotates the refresh token. `onRefreshed` is your one hook to persist the new pair — skip it and the next process restart will present a token the server has already revoked.
|
|
92
|
+
|
|
93
|
+
## `client.fetch()`
|
|
94
|
+
|
|
95
|
+
`client.fetch()` is the call you'll reach for most. Given a URL, it:
|
|
96
|
+
|
|
97
|
+
1. Sends the request.
|
|
98
|
+
2. If the server answers `402 Payment Required` (x402 or MPP), reads the payment challenge and pays it.
|
|
99
|
+
3. Replays the request with proof of payment.
|
|
100
|
+
4. Returns the body, status, and payment metadata together.
|
|
101
|
+
5. Records a run on Zero when you pass a `capabilityId`, so the capability's reliability signal stays current and the call stays reviewable.
|
|
102
|
+
|
|
103
|
+
Endpoints that don't charge pass straight through: a `200` on the first request returns with `payment: null` and `outcome: "success"`. No payment is attempted.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const result = await client.fetch(url, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "content-type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ city: "tokyo" }),
|
|
110
|
+
maxPay: "0.01", // USDC ceiling; an over-cap challenge aborts before signing
|
|
111
|
+
capabilityId: "cap_abc", // attribute the run to a known capability
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
switch (result.outcome) {
|
|
116
|
+
case "success":
|
|
117
|
+
// 2xx/3xx — result.body / result.bodyRaw are populated.
|
|
118
|
+
break;
|
|
119
|
+
case "payment_failed":
|
|
120
|
+
// Couldn't pay: over your maxPay, insufficient balance, or an
|
|
121
|
+
// unrecognized 402 protocol. Nothing settled — generally safe to retry.
|
|
122
|
+
break;
|
|
123
|
+
case "payment_rejected":
|
|
124
|
+
// Paid, but the capability still returned 402 (a settlement/facilitator
|
|
125
|
+
// race). Reconcile via result.warnings and result.payment.
|
|
126
|
+
break;
|
|
127
|
+
case "payment_close_failed":
|
|
128
|
+
// MPP — charged, but the session-close voucher was rejected. The channel
|
|
129
|
+
// is orphaned; reconcile out-of-band via result.payment.session.
|
|
130
|
+
break;
|
|
131
|
+
case "server_error":
|
|
132
|
+
// The capability returned 4xx/5xx (after payment, if any). See
|
|
133
|
+
// result.upstreamError.
|
|
134
|
+
break;
|
|
135
|
+
case "network_error":
|
|
136
|
+
// The request never reached the server.
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const warning of result.warnings ?? []) {
|
|
141
|
+
// e.g. FETCH_WARNINGS.bodyTruncated, FETCH_WARNINGS.mppSessionCloseFailed
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `maxPay`
|
|
146
|
+
|
|
147
|
+
`maxPay` is your hard per-call ceiling, in USDC. The challenge amount is checked against it **before** anything is signed; an over-cap challenge aborts with `outcome: "payment_failed"` and no on-chain spend. Set it on every paid call — it's the guardrail that keeps a mispriced or hostile endpoint from draining the wallet.
|
|
148
|
+
|
|
149
|
+
To put a human in the loop before any spend, read the price up front (`cost.amount` is on every search result and on `capabilities.get()`), show it to your user, and only call `fetch()` with `maxPay` once they approve.
|
|
150
|
+
|
|
151
|
+
## Namespaces
|
|
152
|
+
|
|
153
|
+
Beyond `search()` and `fetch()`, the client groups the rest of the API into resource namespaces:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// Search — ranked capabilities for a query (no auth required)
|
|
157
|
+
const { capabilities } = await client.search("translate text to french", {
|
|
158
|
+
maxCost: "0.05", // optional: only results at or under this price
|
|
159
|
+
protocol: "x402", // optional: "x402" | "mpp"
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Capabilities — full detail for one capability by id
|
|
163
|
+
const cap = await client.capabilities.get("cap_abc");
|
|
164
|
+
|
|
165
|
+
// Wallet (account or session mode)
|
|
166
|
+
const balance = await client.wallet.balance(); // Base + Tempo
|
|
167
|
+
const onBase = await client.wallet.balance({ chain: "base" }); // a single chain
|
|
168
|
+
const fundUrl = await client.wallet.fundingUrl({ amount: "10" }); // onramp link
|
|
169
|
+
|
|
170
|
+
// Runs — fetch() records these for you on paid calls; reach for them
|
|
171
|
+
// directly when you want to log or review a call yourself
|
|
172
|
+
const run = await client.runs.create({
|
|
173
|
+
capabilityId: "cap_abc",
|
|
174
|
+
status: 200,
|
|
175
|
+
latencyMs: 1234,
|
|
176
|
+
});
|
|
177
|
+
await client.runs.review({
|
|
178
|
+
runId: run.runId,
|
|
179
|
+
success: true,
|
|
180
|
+
accuracy: 5, // 1–5
|
|
181
|
+
value: 4, // 1–5
|
|
182
|
+
reliability: 5, // 1–5
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Bug reports
|
|
186
|
+
await client.bugReports.create({
|
|
187
|
+
category: "broken_execution", // see the BugReportCategory union for the full set
|
|
188
|
+
capabilityId: "cap_abc",
|
|
189
|
+
description: "Returns 502 on every call",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Auth (session mode)
|
|
193
|
+
await client.auth.refresh(); // force a token refresh
|
|
194
|
+
await client.auth.logout(refreshToken); // revoke the session server-side
|
|
195
|
+
|
|
196
|
+
// Device-code login (RFC 8628 — for CLIs and other browserless flows)
|
|
197
|
+
const device = await client.auth.device.start();
|
|
198
|
+
console.log(device.verificationUri, device.userCode);
|
|
199
|
+
const grant = await client.auth.device.poll(device.deviceCode);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Multi-tenant servers
|
|
203
|
+
|
|
204
|
+
A server that signs as a different end-user wallet on each request shouldn't build a fresh `ZeroClient` every call. Pass the wallet per call, or derive a reusable sub-client:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const base = new ZeroClient(); // shared transport config, no auth of its own
|
|
208
|
+
|
|
209
|
+
// Simplest: sign a single call as a given wallet.
|
|
210
|
+
await base.fetch(url, { account: tenantAccount, maxPay: "0.01" });
|
|
211
|
+
|
|
212
|
+
// Or hold a sub-client bound to that wallet. `withAccount` caches by address,
|
|
213
|
+
// so repeat calls for the same tenant reuse one instance.
|
|
214
|
+
const tenant = base.withAccount(tenantAccount);
|
|
215
|
+
await tenant.fetch(url, { maxPay: "0.01" });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`withSession(session)` is the session-mode equivalent for per-user token pairs; unlike `withAccount` it isn't cached, since sessions are typically one per request. On a long-running server, call `base.clearAccountCache(account?)` when a tenant churns — pass an account to drop one entry, or omit it to clear the cache entirely.
|
|
219
|
+
|
|
220
|
+
## Errors
|
|
221
|
+
|
|
222
|
+
The SDK throws typed errors instead of strings. Match them with `instanceof`:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import {
|
|
226
|
+
ZeroApiError, // a 4xx/5xx from Zero's own API
|
|
227
|
+
ZeroAuthError, // missing auth, failed refresh, expired session
|
|
228
|
+
ZeroPaymentError, // payment failed before settlement
|
|
229
|
+
ZeroSessionCloseFailedError, // MPP session-close voucher rejected
|
|
230
|
+
ZeroTimeoutError, // the configured timeout was exceeded
|
|
231
|
+
ZeroConfigurationError, // invalid ClientOptions
|
|
232
|
+
ZeroWalletError, // a wallet read or write failed
|
|
233
|
+
ZeroValidationError, // a response didn't match its expected schema
|
|
234
|
+
ZeroError, // base class for all of the above
|
|
235
|
+
} from "@zeroxyz/sdk";
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await client.fetch(url, { maxPay: "0.01" });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (err instanceof ZeroAuthError) {
|
|
241
|
+
// re-auth and retry
|
|
242
|
+
} else if (err instanceof ZeroError) {
|
|
243
|
+
console.error(err.code, err.message);
|
|
244
|
+
} else {
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`client.fetch()` reserves throwing for programmer errors — bad config, missing credentials, and the like. Expected runtime failures (`network_error`, `payment_failed`, an upstream `5xx`) come back as a `FetchResult` with the matching `outcome`, so you handle them by branching on the result rather than by catching.
|
|
251
|
+
|
|
252
|
+
## Logging
|
|
253
|
+
|
|
254
|
+
Pass a `logger` to observe SDK-level events — refreshes, recording failures, payment warnings:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
new ZeroClient({
|
|
258
|
+
logger: (event) => {
|
|
259
|
+
if (event.level === "error") console.error(event.message, event.meta);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Testing
|
|
265
|
+
|
|
266
|
+
The package ships mock helpers under `@zeroxyz/sdk/testing` for unit tests against a stubbed Zero API. They carry no runtime peer dependencies:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { createTestClient, respondJson } from "@zeroxyz/sdk/testing";
|
|
270
|
+
|
|
271
|
+
const client = createTestClient({
|
|
272
|
+
routes: {
|
|
273
|
+
"GET /v1/capabilities/cap_abc": () =>
|
|
274
|
+
respondJson({ id: "cap_abc", name: "Test capability" }),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const cap = await client.capabilities.get("cap_abc");
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
For end-to-end tests against a real payment flow, mock at the network boundary with [MSW](https://mswjs.io/) or run the call against your own staging facilitator.
|