@near-intents-agent-api/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NEAR Intents Agent API contributors
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,330 @@
1
+ # `@near-intents-agent-api/sdk`
2
+
3
+ TypeScript client for the **NEAR Intents Agent API**: give an AI agent its own custody account on
4
+ NEAR Intents, let its owner sign the spending rules once, and let the agent swap, transfer and
5
+ withdraw inside those rules.
6
+
7
+ - One method per endpoint, plain TypeScript types for every request and response.
8
+ - Runs on your **backend**. It holds your API key, so it never belongs in a browser.
9
+ - No wallet signers, no React hooks, no automatic retries. Types only, no runtime schemas.
10
+
11
+ ```sh
12
+ npm install @near-intents-agent-api/sdk
13
+ ```
14
+
15
+ Requires Node 24+. MIT licensed.
16
+
17
+ ## How it fits together
18
+
19
+ | Who | Proves itself with | Does what |
20
+ |---|---|---|
21
+ | **Your backend** | API key (`naa_…`) | Creates agents, reads state, prepares owner actions |
22
+ | **The owner** (NEAR account, EVM wallet or passkey) | A wallet signature, once per change | Creates the agent, sets its rules (the *policy*), issues grants, freezes, deletes |
23
+ | **The agent** (an assistant, bot or session) | A grant token (`ngt_…`) | Swaps, transfers and withdraws inside the owner's policy |
24
+
25
+ The owner signs; your backend never holds the owner's keys. A grant says only *who* may act, and
26
+ the policy says *what* any grant may do, where funds may go and how much.
27
+
28
+ ## Setup
29
+
30
+ Create an API key in the partner dashboard and keep it out of git:
31
+
32
+ ```sh
33
+ # .env
34
+ AGENT_API_KEY=naa_...
35
+ ```
36
+
37
+ ```ts
38
+ import { createAgentApi } from "@near-intents-agent-api/sdk";
39
+
40
+ const api = createAgentApi({ apiKey: process.env.AGENT_API_KEY! });
41
+ ```
42
+
43
+ Run it with `node --env-file=.env your-script.js`. The client talks to
44
+ `https://api.agentsonintents.com` by default. Pass `baseUrl` only for a self-hosted or local
45
+ deployment (HTTPS, or HTTP for `localhost`):
46
+
47
+ ```ts
48
+ const local = createAgentApi({ apiKey: process.env.AGENT_API_KEY!, baseUrl: "http://localhost:3000" });
49
+ ```
50
+
51
+ Check the connection:
52
+
53
+ ```ts
54
+ const network = await api.getNetwork(); // public: service and provider status
55
+ const me = await api.whoami(); // validates the API key; never returns the key
56
+ const tokens = await api.getTokens(); // public: every asset an agent can use
57
+ ```
58
+
59
+ ## Create an agent
60
+
61
+ Creating an agent is an **owner action**: your backend prepares it, the owner's wallet signs it,
62
+ your backend submits it. Every owner action follows the same four steps.
63
+
64
+ ```ts
65
+ import type { Policy } from "@near-intents-agent-api/sdk";
66
+
67
+ // 1. The rulebook. It is always sent complete, and the owner signs all of it at once.
68
+ const policy: Policy = {
69
+ frozen: false,
70
+ actions: ["swap", "transfer"], // any of "swap" | "transfer" | "withdraw"
71
+ confidential: false, // true also allows private balances and confidential routes
72
+ owner_approval: false, // true makes every outgoing action wait for the owner's vote
73
+ assets: "any", // or the exact asset ids the agent may touch
74
+ limits: {
75
+ // optional per-asset caps in atomic units: per_transaction, hourly, daily, monthly
76
+ per_transaction: { "nep141:wrap.near": "1000000000000000000000000" },
77
+ },
78
+ max_actions_per_hour: null,
79
+ destinations: { mode: "only", list: [] }, // no destinations: funds stay in the account
80
+ budget: { daily_usd: "100", weekly_usd: null, monthly_usd: null }, // USD caps across assets
81
+ timelock_ms: 0, // delay before every delegated action runs
82
+ };
83
+
84
+ // 2. Prepare. The response carries the exact payload the owner's wallet must sign.
85
+ const generated = await api.generateIntent({
86
+ type: "agent_create",
87
+ name: "My first agent",
88
+ owner: { type: "near", account_id: "alice.near", public_key: "ed25519:..." },
89
+ policy,
90
+ });
91
+ generated.preview.policy; // show the owner what they are about to authorize
92
+
93
+ // 3. The owner signs `generated.intent` with their wallet (see "Signing" below).
94
+ const signedData = await signWithWallet(generated.intent);
95
+
96
+ // 4. Submit, then follow the operation to its end.
97
+ await api.submitIntent({
98
+ type: generated.type,
99
+ correlation_id: generated.correlation_id,
100
+ signed_data: signedData,
101
+ });
102
+ const status = await settled(generated.correlation_id);
103
+ if (status.status !== "SUCCESS") throw new Error(status.failure_code ?? status.status);
104
+
105
+ const agentId = generated.agent_id;
106
+ const agent = await api.getAgent(agentId);
107
+ const wallet = await api.getWallet(agentId);
108
+ ```
109
+
110
+ `settled` polls the status endpoint, which long-polls for up to 30 seconds per call:
111
+
112
+ ```ts
113
+ async function settled(correlationId: string) {
114
+ while (true) {
115
+ const status = await api.getStatus(correlationId, { waitMs: 30_000 });
116
+ if (status.status !== "PROCESSING" && status.status !== "QUEUED") return status;
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### Signing
122
+
123
+ `generated.intent` is `{ standard, payload }`. The wallet signs `payload` exactly as returned, and
124
+ you submit the wallet's output back. Pick the wallet method from `intent.standard`; never rebuild,
125
+ hash or reorder the payload:
126
+
127
+ ```ts
128
+ async function signWithWallet(intent: GenerateIntentResponse["intent"]): Promise<SignedData> {
129
+ switch (intent.standard) {
130
+ case "nep366": // NEAR: gasless delegate action, submitted by the API's sponsor
131
+ return { ...intent, signed_delegate: (await wallet.signDelegateActions({ delegateActions: [intent.payload] })).signedDelegateActions[0] };
132
+ case "nep413": { // NEAR: message signature
133
+ const nonce = Uint8Array.from(atob(intent.payload.nonce), (c) => c.charCodeAt(0));
134
+ const { publicKey, signature } = await wallet.signMessage({ ...intent.payload, nonce });
135
+ return { ...intent, public_key: publicKey, signature };
136
+ }
137
+ case "eip712": // EVM wallet
138
+ return { ...intent, signature: await walletClient.signTypedData({ account, ...intent.payload }) };
139
+ case "webauthn": // passkey
140
+ return { ...intent, credential: await startAuthentication({ optionsJSON: intent.payload }) };
141
+ }
142
+ }
143
+ ```
144
+
145
+ The SDK contains no signers. The `wallet`, `walletClient` and `startAuthentication` above are your
146
+ own wallet integration (NEAR wallet selector, viem, `@simplewebauthn/browser`). In a web app the
147
+ usual split is: your backend calls `generateIntent` and returns the intent to the browser, the
148
+ browser signs with the user's wallet, and your backend calls `submitIntent` with the result.
149
+
150
+ ## Fund the account
151
+
152
+ Incoming funds need no grant. Ask for a deposit address, send to it, and follow the status:
153
+
154
+ ```ts
155
+ const deposit = await api.deposit(agentId, {
156
+ origin_asset: "nep141:usdt.tether-token.near",
157
+ amount: "2000000", // atomic units: 2 USDT with 6 decimals
158
+ confidential: false,
159
+ });
160
+ // Send externally to deposit.details.deposit_address, then poll getStatus(deposit.correlation_id).
161
+
162
+ const balances = await api.getBalances(agentId);
163
+ ```
164
+
165
+ Amounts are strings in the token's **atomic units**. Asset ids come from `getTokens()`.
166
+
167
+ ## Let an agent act
168
+
169
+ Each assistant, session or bot gets its own **grant**. You create the token, the owner signs only
170
+ its commitment, and the token stays on your backend (encrypted):
171
+
172
+ ```ts
173
+ import { createGrantCredential } from "@near-intents-agent-api/sdk";
174
+
175
+ const { token, commitment } = createGrantCredential();
176
+ const grant = await api.generateIntent({
177
+ type: "grant_issue",
178
+ agent_id: agentId,
179
+ label: "Trading assistant",
180
+ credential: commitment,
181
+ expires_at: new Date(Date.now() + 86_400_000).toISOString(), // at most 365 days ahead
182
+ });
183
+ // The owner signs grant.intent and your backend submits it, exactly as in "Create an agent".
184
+
185
+ const assistant = api.forGrant(token); // one client per grant
186
+ ```
187
+
188
+ The agent then acts within the policy, with no further owner signature:
189
+
190
+ ```ts
191
+ const swap = {
192
+ origin_asset: "nep141:wrap.near",
193
+ destination_asset: "nep141:usdt.tether-token.near",
194
+ amount: "1000000000000000000000000",
195
+ };
196
+ const quote = await assistant.swap(agentId, { ...swap, dry: true }); // preview, moves nothing
197
+ const execution = await assistant.swap(agentId, swap);
198
+ const done = await settled(execution.correlation_id);
199
+
200
+ await assistant.transfer(agentId, { asset: "nep141:wrap.near", amount: "1", recipient: "bob.near" });
201
+ await assistant.withdraw(agentId, { asset: "nep141:wrap.near", amount: "1", chain: "near", recipient: "bob.near" });
202
+ ```
203
+
204
+ If the policy refuses, the call fails with a stable `code` such as `policy_action_denied`,
205
+ `policy_destination_denied`, `spend_budget_exceeded` or `insufficient_balance`; the error says which
206
+ layer refused.
207
+
208
+ ## Change the rules
209
+
210
+ A policy change is one owner signature over the complete new policy. Read it, edit it, keep the
211
+ revision:
212
+
213
+ ```ts
214
+ const current = await api.getPolicy(agentId);
215
+ if (current.policy === null || current.revision === null) throw new Error("policy_not_ready");
216
+
217
+ const update = await api.generateIntent({
218
+ type: "policy_update",
219
+ agent_id: agentId,
220
+ expected_revision: current.revision,
221
+ policy: { ...current.policy, timelock_ms: 60_000 },
222
+ });
223
+ // Owner signs update.intent, backend submits, then follow update.correlation_id.
224
+ ```
225
+
226
+ If someone changed the policy meanwhile, submitting fails with `policy_revision_conflict`: read the
227
+ current policy again and ask for a fresh signature. Other owner actions use the same four steps:
228
+ `agent_freeze`, `agent_unfreeze`, `grant_revoke`, `execution_cancel`, `agent_archive`,
229
+ `agent_restore`, `agent_delete` and `approval_vote`.
230
+
231
+ ## Statuses
232
+
233
+ `getStatus` returns the operation with a `status`:
234
+
235
+ | Status | Meaning | What to do |
236
+ |---|---|---|
237
+ | `PENDING_SIGNATURE` | Waiting for the owner's signature | Get it signed, then `submitIntent` |
238
+ | `PENDING_APPROVAL` | Waiting for the owner's approval vote | The owner votes (`approval_vote`) |
239
+ | `PENDING_DEPOSIT` | Waiting for incoming funds | Send the deposit |
240
+ | `QUEUED`, `PROCESSING` | In progress (queued runs after the policy's timelock) | Keep polling |
241
+ | `SUCCESS` | Done, backed by reconciled evidence | |
242
+ | `REFUNDED`, `FAILED` | Ended without the intended result | Read `failure_code` |
243
+ | `UNCERTAIN` | The outcome is unknown | Keep polling the **original** `correlation_id`; never resubmit under a new key |
244
+ | `NEEDS_REVIEW` | Stopped for inspection | Read `details.reason`; never retry. After it is resolved, `getStatus(id, { refresh: true })` |
245
+
246
+ A failed operation with `details.never_executed` or `details.never_submitted` set to `true` has its
247
+ USD budget charge released once; a new attempt needs a new idempotency key.
248
+
249
+ ## Idempotency
250
+
251
+ Every write (`generateIntent`, `swap`, `withdraw`, `transfer`, `shield`, `unshield`, `deposit`)
252
+ carries an `Idempotency-Key`. Omit it and the SDK generates one and returns it with the result as
253
+ `result.idempotencyKey`. To survive a crash, create and store the key **before** sending:
254
+
255
+ ```ts
256
+ import { createIdempotencyKey } from "@near-intents-agent-api/sdk";
257
+
258
+ const idempotencyKey = createIdempotencyKey();
259
+ await saveRequest({ agentId, request, idempotencyKey }); // your durable storage
260
+ await assistant.transfer(agentId, request, { idempotencyKey });
261
+ ```
262
+
263
+ To retry, send the same body with the same key; the original result comes back. The SDK never
264
+ retries by itself. A request with a new key is new work, so never use one to retry an `UNCERTAIN`
265
+ operation. `recover` requires the original key. Quotes, reads and `submitIntent` need no key.
266
+
267
+ ## Errors
268
+
269
+ Failures throw `AgentApiError` with `status`, a stable snake_case `code` (branch on it, never on
270
+ `title`), `retryable`, `availableAt`, `requestId`, the `errors` list and the transmitted
271
+ `idempotencyKey`:
272
+
273
+ ```ts
274
+ import { AgentApiError } from "@near-intents-agent-api/sdk";
275
+
276
+ try {
277
+ await api.submitIntent(submission);
278
+ } catch (error) {
279
+ if (error instanceof AgentApiError && error.code === "policy_revision_conflict") {
280
+ // Read the current policy, then generate a fresh request and get a fresh signature.
281
+ }
282
+ throw error;
283
+ }
284
+ ```
285
+
286
+ Transport, timeout, cancellation and parsing failures throw `AgentApiRequestError`, and a response
287
+ over the size budget throws `AgentApiResponseTooLargeError`. Both keep the original `idempotencyKey`
288
+ (and `cause`); a failed write may still have reached the API, so reconcile the original operation
289
+ before retrying. Rate limits (`429`) expose `retryable` and `availableAt`.
290
+
291
+ ## Options
292
+
293
+ | Option | Default | |
294
+ |---|---|---|
295
+ | `apiKey` | required | Your `naa_…` key. Backend only. |
296
+ | `baseUrl` | `https://api.agentsonintents.com` | HTTPS origin; HTTP allowed for `localhost` |
297
+ | `grantToken` | none | Prefer `api.forGrant(token)`, which returns a separate client per grant |
298
+ | `timeoutMs` | 150 s for swap, withdraw, transfer, shield, unshield; 65 s otherwise | |
299
+ | `maxResponseBytes` | 8 MiB | Checked before JSON parsing, including errors |
300
+ | `fetch` | global `fetch` | For tests or instrumentation |
301
+
302
+ Every method also takes `{ signal }` to cancel it. Requests omit cookies and reject redirects.
303
+
304
+ ## Methods
305
+
306
+ | Area | Methods |
307
+ |---|---|
308
+ | Owner actions | `generateIntent`, `submitIntent`, `getStatus`, `getHistory` |
309
+ | Agents | `listAgents`, `getAgent`, `getWallet`, `getBalances`, `getAddress`, `getContainment` |
310
+ | Rules and access | `getPolicy`, `getPolicyHistory`, `listGrants`, `listApprovals`, `getApproval`, `listScheduledExecutions` |
311
+ | Agent actions (need a grant) | `swap`, `withdraw`, `transfer`, `shield`, `unshield` |
312
+ | Funding and recovery | `deposit`, `recover` |
313
+ | Account and service | `whoami`, `getPartnerQuota`, `getNetwork`, `getTokens` |
314
+ | Helpers | `createGrantCredential`, `grantCommitment`, `createIdempotencyKey`, `forGrant` |
315
+
316
+ `getPartnerQuota()` reports how many agents and API keys you may create and what you have used.
317
+ Every request, response and view type is exported (`Policy`, `AgentView`, `StatusResponse`,
318
+ `GenerateIntentRequest`, `OwnerWallet`, …).
319
+
320
+ ## Without the SDK
321
+
322
+ The API is plain HTTP. `GET https://api.agentsonintents.com/openapi.json` has every schema and
323
+ `GET https://api.agentsonintents.com/llms.txt` is a compact guide for LLM callers. The types in this
324
+ package are generated from that OpenAPI document, so to validate responses at runtime or to build
325
+ a client in another language, generate it from the same document.
326
+
327
+ ## Contributing
328
+
329
+ See [CONTRIBUTING.md](CONTRIBUTING.md). `src/generated/` is generated from the API's OpenAPI
330
+ document and is not edited by hand. Security reports: [SECURITY.md](SECURITY.md).