@stellar-agent-services/dev-mcp 0.0.0-stage → 0.1.1

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/docs.d.ts +35 -0
  4. package/dist/docs.d.ts.map +1 -0
  5. package/dist/docs.js +92 -0
  6. package/dist/docs.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +6 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/server.d.ts +14 -0
  12. package/dist/server.d.ts.map +1 -0
  13. package/dist/server.js +125 -0
  14. package/dist/server.js.map +1 -0
  15. package/package.json +40 -4
  16. package/repository/AGENTS.md +162 -0
  17. package/repository/CONTRIBUTING.md +95 -0
  18. package/repository/IMPLEMENTATION_PLAN.md +560 -0
  19. package/repository/README.md +80 -0
  20. package/repository/SECURITY.md +22 -0
  21. package/repository/apps/wallet/README.md +78 -0
  22. package/repository/docs/client-integration/README-AI.md +495 -0
  23. package/repository/docs/client-integration/README.md +138 -0
  24. package/repository/docs/dx-improvements.md +670 -0
  25. package/repository/docs/erc-compatibility.md +163 -0
  26. package/repository/docs/job-recovery.md +201 -0
  27. package/repository/docs/payment-client.md +290 -0
  28. package/repository/docs/provider-setup-guide.md +348 -0
  29. package/repository/docs/provider-standard.md +728 -0
  30. package/repository/docs/recovery/README-AI.md +288 -0
  31. package/repository/docs/recovery/README.md +119 -0
  32. package/repository/docs/sow-closeout.md +67 -0
  33. package/repository/docs/standard/README-AI.md +614 -0
  34. package/repository/docs/standard/README.md +108 -0
  35. package/repository/docs/verification.md +504 -0
  36. package/repository/examples/paid-service/README.md +33 -0
  37. package/repository/examples/python-upstream/README.md +44 -0
  38. package/repository/llms.txt +65 -0
  39. package/repository/packages/create-stellar-service/README.md +143 -0
  40. package/repository/packages/dev-mcp/README.md +51 -0
  41. package/repository/packages/payment-client/README.md +98 -0
  42. package/repository/packages/protocol/README.md +15 -0
  43. package/repository/packages/protocol/schemas/manifest.schema.json +272 -0
  44. package/repository/packages/protocol/test-vectors/README.md +45 -0
  45. package/repository/packages/provider-kit/README.md +509 -0
  46. package/repository/packages/provider-kit/schemas/gateway-config.schema.json +140 -0
  47. package/repository/packages/verifier/README.md +131 -0
  48. package/repository/plugins/stellar-agent-services/README.md +31 -0
@@ -0,0 +1,614 @@
1
+ # Provider Standard v1: implementation reference for AI coding agents
2
+
3
+ Use this file to build or review a **provider** (a paid AI service) or a **client** (an agent, wallet or app that
4
+ pays one) that follows Provider Standard v1.
5
+
6
+ - **Source of truth:** [provider-standard.md](../provider-standard.md). If this file and the spec disagree, the
7
+ spec wins. Cite rules by id, such as `[J5]`.
8
+ - **Identifier:** `stellar-agent-services/provider@1`. v1 is frozen and only gains optional fields and SHOULD
9
+ rules.
10
+ - **Key words:** MUST, MUST NOT, SHOULD and MAY as in RFC 2119.
11
+ - **Human summary:** [README.md](README.md).
12
+
13
+ ## 0. Rules for the agent using this file
14
+
15
+ 1. Pick one path in §1 and follow it. Do not mix the kit with a hand-written job flow.
16
+ 2. Do not invent manifest fields, headers, status codes or error codes. Put anything extra in `extensions`, with
17
+ reverse-DNS keys `[X1]`.
18
+ 3. Never put a Stellar secret key in provider code, config or environment. A provider knows only its public
19
+ `payTo` address and a facilitator URL.
20
+ 4. Treat every money path as fail-closed. When you can't tell whether a payment settled, the job is
21
+ `payment_uncertain`, never `failed` and never retried with a new charge `[J13]`.
22
+ 5. Validate your work with the JSON Schema and test vectors in §13 before you call it done.
23
+
24
+ ## 1. Choose a path
25
+
26
+ | Situation | Path | Start from |
27
+ | --- | --- | --- |
28
+ | New service in TypeScript (Node, Workers, Deno, Bun) | **Kit**: `@stellar-agent-services/provider-kit` | §2, [examples/paid-service](../../examples/paid-service) |
29
+ | Existing HTTP API in any language | **Gateway**: `provider-kit serve --config <file>`, or the Docker image. You write a JSON config, no code. | [provider-setup-guide.md §3a](../provider-setup-guide.md#3a-gateway-an-existing-api), [examples/python-upstream](../../examples/python-upstream) |
30
+ | Any other language or stack | **Native**: implement §3–§12 | §3–§12, §13 |
31
+ | Agent, wallet or app that pays providers | **Client** | §14, [packages/payment-client](../../packages/payment-client) |
32
+
33
+ The packages are published on npm from 0.1.0. Use `npm create stellar-service`, or install the package you need.
34
+ For unreleased changes, clone this repository and run `npm install && npm run build`. Then build inside it,
35
+ copy `examples/paid-service`, or link a package with `npm install <repo>/packages/provider-kit`.
36
+
37
+ ## 2. Kit path (TypeScript)
38
+
39
+ ```ts
40
+ import { createProvider, defineService } from "@stellar-agent-services/provider-kit";
41
+
42
+ const textStats = defineService({
43
+ name: "text-stats", // ^[a-z0-9][a-z0-9_-]{0,63}$
44
+ description: "Counts words in English text. Use it to check length limits. Up to 20,000 characters.",
45
+ price: "0.01", // USDC decimal string; becomes "100000" base units
46
+ charge: "on-success", // or "upfront" (the default)
47
+ expectedDurationSeconds: 1, // required for on-success; at most maxTimeoutSeconds / 2
48
+ capacity: { concurrency: 4, maxQueued: 100, maxQueueWaitSeconds: 60 }, // these are the defaults
49
+ inputSchema: { type: "object", required: ["text"], properties: { text: { type: "string", maxLength: 20000 } } },
50
+ outputSchema: { type: "object", required: ["words"], properties: { words: { type: "integer" } } },
51
+ handler: async ({ text }: { text: string }, { signal }) => ({ words: text.split(/\s+/).filter(Boolean).length }),
52
+ });
53
+
54
+ const provider = createProvider({
55
+ baseUrl: "https://api.example.com", // https only; http://localhost needs dev: true
56
+ network: "stellar:testnet",
57
+ payTo: "G...", // public address with a USDC trustline. Never a secret key.
58
+ facilitator: "https://x402.org/facilitator",
59
+ provider: { name: "Example provider" },
60
+ services: [textStats],
61
+ // optional: maxTimeoutSeconds (30–300, default 60), retentionSeconds, holdSeconds (0–30), store,
62
+ // rateLimit: { requests, windowSeconds, key: (request) => string | undefined }, dev
63
+ });
64
+
65
+ // Mount provider.fetch(request) on any Fetch API server. Hono: app.all("*", (c) => provider.fetch(c.req.raw)).
66
+ ```
67
+
68
+ The kit implements §3–§12: the manifest, the challenge, the processing order, jobs, capacity and errors. A handler
69
+ receives only the parsed input and an `AbortSignal`, never payment data. Throw to fail the job. Error messages are
70
+ not passed on to the client `[E2]`.
71
+
72
+ Pass `store: sqliteStore(path)` in production. The default `memoryStore()` loses jobs on restart. With SQLite,
73
+ paid jobs survive restarts, and processes on one machine share one queue
74
+ ([kit README](../../packages/provider-kit/README.md#restarts-several-processes-separate-workers)). For an existing
75
+ API, gateway mode (`provider-kit serve --config <file>`) does all of this from a JSON config.
76
+
77
+ The kit's CLI runs the rest, with no secret key anywhere:
78
+
79
+ | Step | Command | What it does |
80
+ | --- | --- | --- |
81
+ | Run | `provider-kit dev --config <file>`, or `provider-kit dev -- <your server command>` | Runs it on this computer. With `PAY_TO` empty, it is paid to a throwaway testnet account whose key is dropped at once. |
82
+ | Pay once | `provider-kit try [<service>] --yes` | One paid testnet call from a throwaway payer, after the user agreed to pay. Starts the provider when nothing answers (`--start '<command>'` for a library provider). |
83
+ | Go live | `provider-kit launch --config <file>` or `--start '<command>' --url <https origin>` | One approval page in the user's wallet: USDC trustline, 8004 registration [I1]–[I3], validation request. Saves `PAY_TO` and `AGENT_ID`, deploys with `flyctl` when there is a `fly.toml`. Ask the user first; run it with `--no-open` and give them the link. |
84
+ | Where it stands | `provider-kit status --json` | Each stage, from running here to verified, and the command to run next |
85
+
86
+ **Mainnet** (`stellar:pubnet`, real USDC) is the same code with `network: "stellar:pubnet"`, a facilitator that settles
87
+ it, and a Soroban RPC (`rpc`), since pubnet has no default one. OpenZeppelin's facilitator,
88
+ `https://channels.openzeppelin.com/x402`, wants an API key: `httpFacilitator(url, { headers: { Authorization:
89
+ process.env.FACILITATOR_AUTH } })` with `FACILITATOR_AUTH="Bearer <key>"`, or `facilitatorHeaders` in a gateway config.
90
+ There, `dev` needs the owner's own `PAY_TO`, and `try` doesn't run: it pays with test money.
91
+ `create-stellar-service --network mainnet` writes these settings. Build and try on testnet first.
92
+
93
+ ## 3. Constants
94
+
95
+ | Name | Value |
96
+ | --- | --- |
97
+ | `standard` | `stellar-agent-services/provider@1` |
98
+ | Default manifest path | `/.well-known/stellar-agent-services.json` |
99
+ | Manifest size | ≤ 256 KiB, I-JSON (RFC 7493) |
100
+ | Services per manifest | 1–25 |
101
+ | Service name | `^[a-z0-9][a-z0-9_-]{0,63}$` |
102
+ | `inputSchema` / `outputSchema` | JSON Schema 2020-12, root `"type": "object"`, each ≤ 32 KiB as canonical JSON |
103
+ | `maxInputBytes` | default 1,048,576, maximum 10,485,760 |
104
+ | `examples` / `tags` | ≤ 5 / ≤ 10, tag pattern `^[a-z0-9-]{1,32}$` |
105
+ | `price.amount` | `^[1-9][0-9]{0,37}$`, USDC base units, 7 decimals (`"100000"` = 0.01 USDC) |
106
+ | `maxTimeoutSeconds` | 30–300 |
107
+ | `jobs.retentionSeconds` | default 86400, minimum 3600 |
108
+ | Networks (CAIP-2) | `stellar:testnet`, `stellar:pubnet` |
109
+ | USDC asset, testnet | `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` |
110
+ | USDC asset, pubnet | `CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75` |
111
+ | `Idempotency-Key` (job id) | lowercase UUID: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` |
112
+ | `Job-Token` | 32 random bytes, base64url without padding: `^[A-Za-z0-9_-]{43}$` |
113
+ | create-job hold | ≤ 30 s, then `202` |
114
+ | `Retry-After` | whole seconds; `202` responses use 1–60 |
115
+ | Problem `type` | `https://algorialabs.github.io/stellar-agent-services/provider/1#error-<code>` (provisional) |
116
+
117
+ ## 4. Manifest
118
+
119
+ Required: `standard`, `provider.name`, `baseUrl`, `payment`, `services[]`, and for each service `name`,
120
+ `description`, `price.amount`, `inputSchema` and `outputSchema`. `expectedDurationSeconds` is required when
121
+ `charge` is `on-success`.
122
+
123
+ Minimal valid manifest (from `test-vectors/manifests.json`, base `minimal`):
124
+
125
+ ```json
126
+ {
127
+ "standard": "stellar-agent-services/provider@1",
128
+ "provider": { "name": "Minimal Provider" },
129
+ "baseUrl": "https://minimal.example.com",
130
+ "payment": {
131
+ "protocol": "x402",
132
+ "x402Version": 2,
133
+ "accepts": [
134
+ {
135
+ "scheme": "exact",
136
+ "network": "stellar:testnet",
137
+ "asset": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
138
+ "payTo": "GBZSBASBFMV5RGKPRIPCB5QQVAJ35IXKMTF2A2766MOA4ZHQ7MLSIPXU",
139
+ "maxTimeoutSeconds": 60,
140
+ "extra": { "areFeesSponsored": true }
141
+ }
142
+ ]
143
+ },
144
+ "services": [
145
+ {
146
+ "name": "echo",
147
+ "description": "Returns its input unchanged. Useful for testing a client.",
148
+ "price": { "amount": "1" },
149
+ "inputSchema": { "type": "object" },
150
+ "outputSchema": { "type": "object" }
151
+ }
152
+ ]
153
+ }
154
+ ```
155
+
156
+ Optional fields:
157
+ - `provider`: `description`, `homepage`, `agentId` (u32), `termsUrl`, `dataUse.storesInputs`,
158
+ `dataUse.trainsOnInputs`
159
+ - top level: `jobs.retentionSeconds`
160
+ - per service: `title`, `charge` (default `upfront`), `expectedDurationSeconds`, `examples`, `maxInputBytes`,
161
+ `tags`, `capacity.concurrency` (≥ 1), `capacity.maxQueueWaitSeconds` (≥ 0)
162
+ - top level and per service: `extensions`
163
+
164
+ Serving and validating it:
165
+ - Serve it over HTTPS with status `200` and `Content-Type: application/json` `[M1]`, and SHOULD send
166
+ `Cache-Control: max-age` of 300 or less `[M4]`.
167
+ - `baseUrl` is HTTPS, has the manifest's origin, and has no user info, query, fragment, empty path segment or
168
+ trailing slash `[M5]`.
169
+ - Every URL is HTTPS, at most 300 characters, without user info. Loopback `http://` is allowed only in a dev mode
170
+ `[M6]`.
171
+ - Validate in three steps: fetch rules `[M1] [M3]`, then the JSON Schema
172
+ ([manifest.schema.json](../../packages/protocol/schemas/manifest.schema.json)) `[M9]`, then the rules the
173
+ schema can't express (`checkManifestRules()`): M1, M3, M5, M7, S6, S9, P11.
174
+ - Descriptions say what the service does, when to use it, and its limits. They contain no prices, URLs or
175
+ instructions to the agent (§3.4).
176
+ - Schemas use the portable subset: `type properties required additionalProperties items enum const minimum maximum
177
+ minLength maxLength minItems maxItems pattern format title description default examples anyOf $defs` with local
178
+ `$ref`. Avoid `if/then/else`, `patternProperties`, and `oneOf`/`allOf` at the root `[S7]`. Pass large media as
179
+ HTTPS URLs (`"format": "uri"`). URLs in an output stay valid for `retentionSeconds` `[S8]`.
180
+
181
+ ## 5. Endpoints and headers
182
+
183
+ | Operation | Request | Auth | Returns |
184
+ | --- | --- | --- | --- |
185
+ | manifest | `GET <manifest URL>` | none | manifest |
186
+ | create-job | `POST {baseUrl}/services/{name}/jobs`, body = the input object, `Content-Type: application/json` | x402 payment | job view, `402`, or a problem |
187
+ | check-status | `GET {baseUrl}/jobs/{jobId}` | `Job-Token` | job view without `output` |
188
+ | get-result | `GET {baseUrl}/jobs/{jobId}/result` | `Job-Token` | job view with `output` when succeeded |
189
+
190
+ | Header | Direction | Meaning |
191
+ | --- | --- | --- |
192
+ | `Idempotency-Key` | client → provider | The job id. Send both it and `Job-Token`, or neither `[J1]`. |
193
+ | `Job-Token` | client → provider | Bearer secret for the job `[J3]` |
194
+ | `PAYMENT-REQUIRED` | provider → client | base64 x402 v2 `PaymentRequired`, on `402` `[P3]` |
195
+ | `PAYMENT-SIGNATURE` | client → provider | base64 x402 v2 `PaymentPayload` |
196
+ | `PAYMENT-RESPONSE` | provider → client | base64 x402 `SettleResponse`, on the response that settled `[P10]` |
197
+ | `Location` | provider → client | `{baseUrl}/jobs/{jobId}` on a create-job `202` |
198
+ | `Retry-After` | provider → client | Whole seconds, on `202`, `409`, `429` and `503` |
199
+
200
+ CORS `[J17]` (SHOULD), on the manifest and all three endpoints:
201
+ - `Access-Control-Allow-Origin: *`
202
+ - `Access-Control-Expose-Headers: PAYMENT-REQUIRED, PAYMENT-RESPONSE, Location, Retry-After`
203
+ - `OPTIONS` preflight: `Access-Control-Allow-Methods: GET, POST` and
204
+ `Access-Control-Allow-Headers: Content-Type, Idempotency-Key, Job-Token, PAYMENT-SIGNATURE`
205
+
206
+ ## 6. create-job algorithm (provider)
207
+
208
+ Follow this order exactly `[§5.4]`. The first failing step answers.
209
+
210
+ ```
211
+ POST {baseUrl}/services/{name}/jobs
212
+ 1. Headers and body
213
+ - only one of Idempotency-Key / Job-Token → 400 invalid_request
214
+ - malformed Idempotency-Key or Job-Token → 400 invalid_request
215
+ - Content-Type is not application/json → 400 invalid_request
216
+ - body larger than maxInputBytes → 413 payload_too_large
217
+ (the service's limit; apply the 10 MiB cap before the lookup)
218
+ - body is not valid UTF-8 JSON → 400 invalid_request
219
+ - body is not a JSON object → 400 invalid_input
220
+ 2. Unknown service → 404 service_not_found
221
+ 3. The job id exists [J5–J7]
222
+ - other binding hash or other token → 422 idempotency_key_reused
223
+ - still being claimed, verified or settled → 409 request_in_progress + Retry-After
224
+ - otherwise → the existing job view (200/202). No new 402.
225
+ Never settle a payment sent with it.
226
+ 4. Input fails inputSchema → 400 invalid_input, errors[] = [{ pointer, message }]
227
+ 5. Capacity [P8][Q1–Q3]
228
+ - caller over its rate limit → 429 rate_limited + Retry-After
229
+ - no slot and no room in the queue → 503 unavailable + Retry-After
230
+ 6. No PAYMENT-SIGNATURE → 402 + PAYMENT-REQUIRED (§7)
231
+ 7. Decode and verify the payment with the facilitator
232
+ - undecodable, or `accepted` doesn't match → 402 (x402 error e.g. "invalid_payment")
233
+ - facilitator says invalid → 402 (x402 error = its reason)
234
+ - facilitator unreachable → 503 unavailable ("nothing was charged")
235
+ 8. In ONE atomic step, claim the job id (binding hash, sha256(token)) AND the payment fingerprint [J4][P9]
236
+ - payment fingerprint already used → 402, x402 error "payment_already_used"
237
+ - an identical request won the race → that job's view; do not settle [J5]
238
+ 9. Settle and run in the order set by `charge` (§8)
239
+ 10. Respond: wait up to 30 s [J9]
240
+ - job finished → 200 (+ PAYMENT-RESPONSE if this response settled)
241
+ - otherwise → 202 + Location + Retry-After
242
+ ```
243
+
244
+ Without `Idempotency-Key` and `Job-Token`, the provider generates both and returns them once, as `jobId` and
245
+ `jobToken` in the create-job response `[J2]`.
246
+
247
+ **Binding hash** `[§5.3]`: `bindingHash = lowercase hex(sha256(JCS({"service": name, "input": input})))`, where JCS
248
+ is RFC 8785 canonical JSON. For example, service `text-stats` with input `{"text":"Hello world."}` canonicalizes to
249
+ `{"input":{"text":"Hello world."},"service":"text-stats"}` and hashes to
250
+ `3d3b3dbdf90621535bd77d1f1d2a1ed0334bf875c7dc7c023caa48cad949ce87`.
251
+
252
+ **Storage:** keep `jobId`, `service`, `charge`, `bindingHash`, `sha256(Job-Token)` (never the token itself), the
253
+ status, timestamps, `output`, `error` and the payment receipt. Make the job id and the payment fingerprint unique
254
+ in the same transaction. Compare token hashes in constant time `[J3]`.
255
+
256
+ ## 7. Payment (x402 v2, `exact` scheme on Stellar)
257
+
258
+ - The manifest has exactly one `payment.accepts` entry, with no `amount` `[P1]`. A `G…` `payTo` has a USDC
259
+ trustline `[P2]` and every `payTo` is a valid strkey `[P11]`. Muxed `M…` addresses are not allowed.
260
+ - **Challenge:** the `402` carries a `PAYMENT-REQUIRED` header `[P3]` that decodes to:
261
+
262
+ ```json
263
+ {
264
+ "x402Version": 2,
265
+ "resource": { "url": "{baseUrl}/services/{name}/jobs", "description": "…", "mimeType": "application/json" },
266
+ "accepts": [{ "scheme": "exact", "network": "…", "asset": "…", "payTo": "…", "amount": "<price.amount>",
267
+ "maxTimeoutSeconds": 60, "extra": { "areFeesSponsored": true } }]
268
+ }
269
+ ```
270
+
271
+ - `accepts` has exactly one entry, equal to the manifest entry plus `amount` = the service's `price.amount` `[P4]`.
272
+ - `resource.url` is the public HTTPS create-job URL `[P5]`.
273
+ - It SHOULD include x402's Bazaar extension `[P6]`.
274
+ - **Paid request:** the same request plus `PAYMENT-SIGNATURE`. Reject a payload whose `x402Version` isn't 2, or
275
+ whose `accepted` differs from the challenge in `scheme`, `network`, `asset`, `amount` or `payTo`.
276
+ - **Facilitator:** verify and settle through an x402 facilitator that supports the network with scheme `exact` and
277
+ sponsored fees, such as `https://x402.org/facilitator` on testnet, or OpenZeppelin's
278
+ `https://channels.openzeppelin.com/x402` on pubnet, which wants an API key. The provider never signs anything.
279
+ - **Settlement outcome:**
280
+ - `settled`: success with a transaction hash.
281
+ - `failed`: only when the facilitator said success is false and gave no transaction.
282
+ - `uncertain`: everything else, including timeouts, 5xx and exceptions. The job becomes `payment_uncertain` and
283
+ is decided later from on-chain data, never by charging again `[J13]`.
284
+ - **One payment, one job:** a payment fingerprint is unique `[P9]`.
285
+
286
+ ## 8. Charge policies, states and the job view
287
+
288
+ | `charge` | Order | If the work fails |
289
+ | --- | --- | --- |
290
+ | `on-success` | verify → queued → running → work → validate output → settle | Not charged. Settle only if the output matches `outputSchema` `[S1]`. |
291
+ | `upfront` | verify → settle → queued → running → work | Charged. v1 has no refunds. |
292
+
293
+ - `[S2]`: the signed payment expires about `maxTimeoutSeconds` after signing.
294
+ - An `on-success` job whose work and settlement don't finish in time fails, uncharged.
295
+ - Keep `expectedDurationSeconds` ≤ `maxTimeoutSeconds / 2`, and use `upfront` for longer work.
296
+
297
+ **Transitions** (anything else is forbidden):
298
+
299
+ ```
300
+ (new) → queued | running | payment_uncertain | failed
301
+ queued → running
302
+ running → succeeded | failed | payment_uncertain (→ payment_uncertain: on-success only)
303
+ payment_uncertain → queued | succeeded | failed (decided from on-chain data)
304
+ succeeded, failed → final [J12]
305
+ ```
306
+
307
+ **`charged`:**
308
+ - `queued` and `running`: `true` for `upfront`, `false` for `on-success`.
309
+ - `succeeded`: `true`.
310
+ - `failed`: `true` for `upfront`, except when `error.code` is `payment_failed`; `false` for `on-success`.
311
+ - `payment_uncertain`: `"unknown"`.
312
+
313
+ **Job view** (the body of create-job, check-status and get-result):
314
+
315
+ ```json
316
+ {
317
+ "jobId": "0192d3a4-7c1e-7b2a-9f00-3c5e8a1b2c3d",
318
+ "service": "text-stats",
319
+ "status": "queued | running | succeeded | failed | payment_uncertain",
320
+ "charge": "on-success | upfront",
321
+ "charged": true,
322
+ "createdAt": "RFC 3339", "updatedAt": "RFC 3339", "expiresAt": "RFC 3339",
323
+ "output": { "…": "create-job and get-result, only when succeeded; withheld while payment_uncertain [J14]" },
324
+ "error": { "type": "…#error-timeout", "title": "…", "code": "service_failed | output_invalid | timeout | payment_failed", "detail": "…" },
325
+ "payment": { "network": "…", "asset": "…", "amount": "…", "payTo": "…", "payer": "G…", "transaction": "<tx hash>", "settledAt": "RFC 3339" },
326
+ "queue": { "position": 1, "estimatedStartAt": "RFC 3339" },
327
+ "jobToken": "only in the create-job response, only when the provider generated it [J2]"
328
+ }
329
+ ```
330
+
331
+ - `error` is present only when `failed`, `payment` only when `charged` is `true`, and `queue` only when `queued`
332
+ `[Q5]`.
333
+ - `200` when the job is finished, otherwise `202` with `Retry-After` `[J8]`.
334
+ - check-status and get-result never take a payment or cause a charge `[J11]`.
335
+ - An unknown job, an expired job, and a missing or wrong token all return the same `404 job_not_found` `[J10]`.
336
+ - Jobs stay retrievable for `retentionSeconds` after they finish `[J15]`.
337
+
338
+ ## 9. Capacity and queuing `[§5.8]`
339
+
340
+ - Check capacity at step 5, before the `402` and before verifying a payment. Check it again on the paid request.
341
+ - `503 unavailable`: the service is full.
342
+ - `429 rate_limited`: this caller sent too many requests.
343
+ - Both carry `Retry-After`, and neither charges anything `[Q1] [Q3]`.
344
+ - Admit a job only if it should start within `capacity.maxQueueWaitSeconds`. For `on-success`, it must also finish
345
+ work and settlement before the payment expires `[Q2]`.
346
+ - An accepted job is not failed because of load.
347
+ - It waits as `queued`, and a paid `upfront` job SHOULD survive a restart.
348
+ - An `on-success` job that can't start before its payment expires goes `running` → `failed` with `timeout`,
349
+ uncharged `[Q4]`.
350
+ - Report the queue in the job view: `"queue": { "position": 1, "estimatedStartAt": "…" }`. Position 1 starts next
351
+ `[Q5]`.
352
+ - A `202`'s `Retry-After` reflects when the job will likely change, 1–60 s `[Q6]`.
353
+ - Never apply capacity limits to check-status or get-result `[Q7]`.
354
+ - Reference admission rule, as in the kit:
355
+ - A service has `concurrency` slots, and `ahead = running + admitted-but-unclaimed + waiting`.
356
+ - When `ahead < concurrency`, the job starts now.
357
+ - Otherwise `position = ahead − concurrency + 1` and `wait = ceil(position / concurrency) × average job seconds`.
358
+ Reject when `position > maxQueued` or `wait > maxQueueWaitSeconds`.
359
+
360
+ ## 10. Errors
361
+
362
+ Every error other than `402` is RFC 9457, sent as `Content-Type: application/problem+json`:
363
+
364
+ ```json
365
+ { "type": "https://algorialabs.github.io/stellar-agent-services/provider/1#error-invalid_input",
366
+ "title": "Input does not match the service's inputSchema", "status": 400, "code": "invalid_input",
367
+ "detail": "optional, written by the provider", "errors": [{ "pointer": "/text", "message": "must be a string" }] }
368
+ ```
369
+
370
+ | `code` | HTTP | When | `Retry-After` |
371
+ | --- | --- | --- | --- |
372
+ | `invalid_request` | 400 | Malformed JSON, wrong content type, bad header, or only one of `Idempotency-Key` / `Job-Token` | |
373
+ | `invalid_input` | 400 | Input fails `inputSchema`; `errors[]` has JSON Pointers | |
374
+ | `service_not_found` | 404 | No such service | |
375
+ | `job_not_found` | 404 | Unknown or expired job, or a wrong or missing token | |
376
+ | `request_in_progress` | 409 | The job id is being claimed, verified or settled `[J7]` | yes |
377
+ | `payload_too_large` | 413 | Body over `maxInputBytes` | |
378
+ | `idempotency_key_reused` | 422 | Same job id, different request `[J6]` | |
379
+ | `rate_limited` | 429 | This caller sent too many requests `[Q1]` | yes |
380
+ | `unavailable` | 503 | At capacity, or a dependency is down | yes |
381
+
382
+ - Job failure codes, in the job view's `error.code`: `service_failed`, `output_invalid`, `timeout` and
383
+ `payment_failed`. A failed job's `error` has no `status`.
384
+ - `[E1]` Clients act on `code`, never on `title` or `detail`.
385
+ - `[E2]` Problems never contain request headers, payment payloads or job tokens. Never copy a handler's exception
386
+ message into `detail`.
387
+
388
+ ## 11. Identity (Stellar 8004)
389
+
390
+ - `[I1]` The provider's 8004 registration file lists
391
+ `{ "name": "stellar-agent-services", "endpoint": "<manifest URL>", "version": "1" }` in `services`. It SHOULD
392
+ also list `{ "name": "web", "endpoint": "<baseUrl>" }` and set the `x402` flag.
393
+ - `[I2]` `provider.agentId` in the manifest is that registration's id.
394
+ - `[I3]` The manifest's `payTo` equals `get_agent_wallet(agentId)` on chain.
395
+ - Registration and validation requests are owner actions, never done in the provider runtime. The owner approves
396
+ them in their own wallet, on a local page the CLI opens: `provider-kit register`, then `provider-kit
397
+ request-validation --validator <verifier G…>`, which work for any provider with `--manifest <url>`. Register from
398
+ the `payTo` account, so the agent wallet is `payTo` at once [I3]. In scripts and CI, `--source <key>` signs
399
+ through the Stellar CLI keychain instead.
400
+ - A validation request's `request_hash` is the SHA-256 of its document's canonical JSON, and its `request_uri` holds
401
+ exactly those bytes ([verification.md](../verification.md#stellar-8004-validation-registry)). The validator is never
402
+ the agent's owner or wallet.
403
+
404
+ ## 12. Security invariants (provider)
405
+
406
+ - No Stellar secret key in the runtime, config, environment or logs.
407
+ - Store `sha256(Job-Token)` only, and compare in constant time.
408
+ - A `404` never reveals whether a job exists.
409
+ - One payment buys at most one job. A repeated request never settles again.
410
+ - An uncertain settlement is never retried with a new charge.
411
+ - Schemas never reference remote documents `[S5]`.
412
+ - Output is validated before settling (`on-success`) and before storing.
413
+ - Problems and logs sent to clients never echo headers, payment payloads or tokens.
414
+ - `description`, `title` and `examples` are untrusted text for clients. Write them as plain descriptions, not as
415
+ instructions.
416
+
417
+ ## 13. Testing and conformance
418
+
419
+ **Test vectors** in [packages/protocol/test-vectors](../../packages/protocol/test-vectors). Each file explains
420
+ itself in its `description`.
421
+
422
+ | File | Pins |
423
+ | --- | --- |
424
+ | `manifests.json` | Manifest validation: JSON Schema cases and code-rule cases, as RFC 6902 patches to a base |
425
+ | `challenges.json` | `checkChallenge`: a `402` versus the approved manifest `[P3–P5]` |
426
+ | `canonical-json.json` | RFC 8785 output, including number formatting |
427
+ | `binding-hash.json` | Binding hashes `[§5.3]` |
428
+ | `headers.json` | `Idempotency-Key` and `Job-Token` patterns, and `Retry-After` parsing |
429
+ | `job-states.json` | Allowed transitions and `charged` per status, charge and failure code |
430
+ | `problems.json` | Error codes, HTTP statuses and which ones carry `Retry-After` |
431
+ | `validation-requests.json` | Validation request documents: canonical bytes, `request_hash` and the `data:` URI |
432
+
433
+ **TypeScript helpers** in `@stellar-agent-services/protocol`, which has no runtime dependencies:
434
+ `checkManifestRules`, `checkChallenge`, `canonicalJson`, `canonicalSha256`, `bindingHash`, `canTransition`,
435
+ `chargedFor`, `jobViewHttpStatus`, `parseRetryAfter`, `problem`, `jobError`, `createJobUrl`, `jobStatusUrl`,
436
+ `jobResultUrl`, `stellarAddressKind`, `validationRequestHash`, `validationRequestDataUri`, `USDC_ASSET`.
437
+
438
+ **A real paid call on testnet** against a local provider, in any language. `provider-kit try` pays once from a
439
+ throwaway testnet account it funds itself, then confirms the transfer on chain. It shows the payment and asks
440
+ first; `--yes` skips the question, and `--json` prints the outcome as JSON:
441
+
442
+ ```bash
443
+ npx provider-kit try text-stats '{"text":"Hello agents."}' --manifest http://localhost:8787/.well-known/stellar-agent-services.json
444
+ ```
445
+
446
+ Or pay from a payer wallet of your own with the payment client CLI (see
447
+ [packages/payment-client/README.md](../../packages/payment-client/README.md)):
448
+
449
+ ```bash
450
+ npx stellar-pay wallet create
451
+ npx stellar-pay wallet fund --usdc 2
452
+ npx stellar-pay pay http://localhost:8787/.well-known/stellar-agent-services.json text-stats '{"text":"Hello agents."}' --dev
453
+ ```
454
+
455
+ **Acceptance checklist for a provider:**
456
+ - [ ] The manifest passes the JSON Schema and `checkManifestRules` with no violations.
457
+ - [ ] Invalid input returns `400 invalid_input` and never a `402`.
458
+ - [ ] An unpaid valid request returns a `402` whose challenge passes `checkChallenge`.
459
+ - [ ] A paid request returns `200`/`202`, and the job view's `payment.transaction` is a real transfer to `payTo`.
460
+ - [ ] Repeating the paid request (same id, token and input) returns the same job, with exactly one transfer on
461
+ chain.
462
+ - [ ] The same id with another input returns `422`. A wrong token returns `404`.
463
+ - [ ] A facilitator timeout during settlement gives `payment_uncertain`, not `failed`.
464
+ - [ ] A burst of 20 simultaneous requests gets only `402`, `429` or `503` with `Retry-After`. No `500`, no
465
+ timeout.
466
+ - [ ] check-status and get-result work while the service is at capacity.
467
+ - [ ] CORS headers are present, including on `OPTIONS`.
468
+
469
+ **Conformance levels** `[§8]`:
470
+ - **1 Described:** C1 manifest + C6 hygiene.
471
+ - **2 Payable:** + C3 challenge + C4 paid call.
472
+ - **3 Recoverable:** + C5 recovery.
473
+ - Independent of the levels: **Registered** (C2 identity + a Validation Registry record) and **Load-safe** (C7
474
+ load).
475
+ - The checks are defined in [verification.md](../verification.md). Run them with
476
+ `npx @stellar-agent-services/verifier check <manifest-url>` (`--dev` for `http://localhost`): every check that
477
+ needs no payment. Exit code 0 means every check that ran passed; each failure names its rule id.
478
+
479
+ ## 14. Client rules (agents, wallets, apps)
480
+
481
+ - Fetch the manifest with no redirects, at most 256 KiB, as I-JSON `[M3]`.
482
+ - Ignore unknown fields, and reject another major `standard` `[M8]`.
483
+ - Never fetch remote schemas `[S5]`.
484
+ - Before the first request, generate and **persist** a UUID job id and a 32-byte token `[J1]`. Recovery depends on
485
+ them.
486
+ - Unpaid create-job → `402`. Compare the challenge with the approved manifest, and refuse to sign on any mismatch
487
+ `[P7]`.
488
+ - Show the price, recipient, network and `charge` before approval `[S3]`.
489
+ - Enforce configured verification limits from trusted evidence matching the approved manifest, payee and network
490
+ `[V1]`. Missing, unreadable or expired evidence blocks a payment requiring verification. Recheck before signing.
491
+ - Sign once. Send the paid request with the same id, token and input.
492
+ - `429` / `503` from create-job: wait `Retry-After` plus jitter, then send the identical request again `[Q8]`.
493
+ - On a paid request, send the same `PAYMENT-SIGNATURE`.
494
+ - Stop before the payment expires, and give up after a bounded wait.
495
+ - The user was not charged.
496
+ - `202`: poll check-status or get-result no faster than `Retry-After`. A `429` or `503` while polling means "later"
497
+ `[Q9]`. Use an HTTP timeout above 30 s `[J9]`.
498
+ - Lost response, crash or timeout after sending payment: recover with get-result. **Never sign or pay again** for
499
+ that job.
500
+ - `payment_uncertain`: stop polling and tell the user not to pay again. Only the provider resolves it `[J13]`.
501
+ - Show "paid" only after confirming the transaction on chain against what the user approved, not against the
502
+ receipt. One transaction proves one job `[J16]`.
503
+ - Label `description`, `title` and `examples` as provider-supplied when you show them to an LLM (§10 of the spec).
504
+ - Map each service 1:1 to an MCP tool with `name`, `title`, `description`, `inputSchema` and `outputSchema`. Keep
505
+ price, network and `charge` out of the tool definition (Appendix B).
506
+
507
+ ## 15. Common mistakes
508
+
509
+ | Mistake | Rule |
510
+ | --- | --- |
511
+ | Answering `402` before validating input or checking capacity | P8, Q1 |
512
+ | `amount` in the manifest's payment option, or more than one `accepts` entry | P1 |
513
+ | `baseUrl` with a trailing slash, or on another origin than the manifest | M5 |
514
+ | Settling again when a known job id is repeated | J5 |
515
+ | Deriving the job id from the input instead of `Idempotency-Key` | J4 |
516
+ | Different responses for "unknown job" and "wrong token" | J10 |
517
+ | Storing the raw `Job-Token` | J3 |
518
+ | Echoing headers, payment payloads, tokens or exception messages in errors | E2 |
519
+ | Marking a facilitator timeout as `failed`, or settling again | J13 |
520
+ | Answering `413` or `404` before reading the request body, and leaving the rest of it on a kept-alive connection: the client's next request on it fails. Read and drop the rest, or close the connection. | Q1 |
521
+ | Long `on-success` work (more than `maxTimeoutSeconds / 2`) | S2 |
522
+ | `500`, a dropped connection or a hang under load instead of `503` + `Retry-After` | Q1 |
523
+ | Rate-limiting check-status or get-result | Q7 |
524
+ | `Retry-After` as a decimal, or missing on `409`, `429` or `503` | §6, Q1 |
525
+ | Holding create-job open longer than 30 s | J9 |
526
+ | CORS without `Access-Control-Expose-Headers`, so browsers can't read `PAYMENT-REQUIRED` | J17 |
527
+ | A non-object schema root, or a remote `$ref` | S4, S5 |
528
+ | Price, URLs or agent instructions in `description` | §3.4 |
529
+ | Changing a finished job | J12 |
530
+ | A muxed `M…` `payTo` | P11, §4.1 |
531
+
532
+ ## 16. Rule index
533
+
534
+ Manifest:
535
+ - **M1** The manifest is served over HTTPS, `200`, `application/json`.
536
+ - **M2** Any URL. The default is `/.well-known/stellar-agent-services.json`, and the 8004 registration points to
537
+ the actual URL.
538
+ - **M3** Clients don't follow redirects. At most 256 KiB, I-JSON.
539
+ - **M4** SHOULD send `Cache-Control: max-age` ≤ 300.
540
+ - **M5** `baseUrl` is HTTPS on the manifest's origin, with no user info, query, fragment, empty segment or trailing
541
+ `/`.
542
+ - **M6** URLs are HTTPS, ≤ 300 characters, without user info. Loopback `http://` only in dev mode.
543
+ - **M7** Service names are unique.
544
+ - **M8** Clients ignore unknown fields and reject another major `standard`.
545
+ - **M9** The manifest is valid against the JSON Schema.
546
+
547
+ Services:
548
+ - **S1** `on-success` settles only for output that is valid against `outputSchema`.
549
+ - **S2** A payment expires about `maxTimeoutSeconds` after signing; overrunning `on-success` work fails uncharged.
550
+ SHOULD keep the expected duration ≤ half of it.
551
+ - **S3** Clients show `charge` before approval.
552
+ - **S4** Schema roots are `"type": "object"`.
553
+ - **S5** No external `$ref` or `$dynamicRef`; `$schema` is 2020-12 only; clients never fetch remote schemas.
554
+ - **S6** Each schema is ≤ 32 KiB as canonical JSON.
555
+ - **S7** SHOULD use the portable JSON Schema subset.
556
+ - **S8** URLs in an output stay valid for `retentionSeconds`.
557
+ - **S9** Examples validate against their schemas.
558
+
559
+ Payment:
560
+ - **P1** Exactly one `accepts` entry, without `amount`.
561
+ - **P2** A `G…` `payTo` has a USDC trustline.
562
+ - **P3** Unpaid create-job returns `402` with `PAYMENT-REQUIRED` (x402 v2).
563
+ - **P4** The challenge's single entry equals the manifest's, plus `amount` = the price.
564
+ - **P5** `resource.url` is the public HTTPS create-job URL.
565
+ - **P6** SHOULD include the Bazaar extension.
566
+ - **P7** Clients compare the challenge with the approved manifest and refuse to sign on a mismatch.
567
+ - **P8** Validate the input and check capacity before any `402`.
568
+ - **P9** One payment buys at most one job; a reused one gets `402` `payment_already_used`.
569
+ - **P10** SHOULD send `PAYMENT-RESPONSE` on the response that settled.
570
+ - **P11** `payTo` is a valid strkey.
571
+
572
+ Jobs:
573
+ - **J1** Clients SHOULD send `Idempotency-Key` and `Job-Token`, MUST send both or neither, and save them before
574
+ paying.
575
+ - **J2** If both are missing, the provider generates them and returns them once.
576
+ - **J3** Store `sha256(Job-Token)` only, and compare in constant time.
577
+ - **J4** A job id is bound to one binding hash and one token hash forever.
578
+ - **J5** Same id, binding and token returns the existing job: no new `402`, and an attached payment is never
579
+ settled.
580
+ - **J6** Same id with another binding or token returns `422 idempotency_key_reused`.
581
+ - **J7** A job id still being claimed or settled returns `409 request_in_progress` with `Retry-After`.
582
+ - **J8** Finished returns `200`; otherwise `202` with `Retry-After`, plus `Location` on create-job.
583
+ - **J9** Hold create-job ≤ 30 s, then `202`. Clients use a timeout above 30 s.
584
+ - **J10** Unknown, expired and wrong-token jobs all return `404 job_not_found`.
585
+ - **J11** check-status and get-result never take a payment or charge.
586
+ - **J12** `succeeded` and `failed` are final; a retry uses a new job id.
587
+ - **J13** An uncertain settlement is never re-charged; it is decided on chain, and `failed` with `charged: false`
588
+ if it wasn't settled.
589
+ - **J14** Output is withheld while `payment_uncertain`.
590
+ - **J15** Jobs stay retrievable for `retentionSeconds` after they finish.
591
+ - **J16** Clients confirm the transaction on chain against what they approved; one transaction proves one job.
592
+ - **J17** SHOULD allow CORS on the manifest and the three endpoints.
593
+
594
+ Capacity:
595
+ - **Q1** SHOULD answer `503` when full and `429` when over the rate limit, with `Retry-After`, before payment.
596
+ - **Q2** SHOULD admit a job only if it should start within `maxQueueWaitSeconds`, and `on-success` jobs only if
597
+ they finish before the payment expires.
598
+ - **Q3** SHOULD check capacity again on the paid request; a `429` or `503` means nothing was charged.
599
+ - **Q4** Load SHOULD NOT fail an accepted job, and queued `upfront` jobs SHOULD survive restarts.
600
+ - **Q5** A queued job SHOULD include `queue: { position, estimatedStartAt }`.
601
+ - **Q6** A `202`'s `Retry-After` SHOULD reflect the next likely change, 1–60 s.
602
+ - **Q7** SHOULD keep check-status and get-result outside capacity limits.
603
+ - **Q8** Clients SHOULD retry a busy create-job with the identical request and the same signed payment, within the
604
+ payment's validity.
605
+ - **Q9** Clients SHOULD poll no faster than `Retry-After`; a busy answer while polling means "later".
606
+
607
+ Errors, identity and extensions:
608
+ - **E1** Every problem has a `code`, and clients act on it.
609
+ - **E2** Problems never contain headers, payment payloads or job tokens.
610
+ - **I1** The 8004 registration lists the manifest under `stellar-agent-services`.
611
+ - **I2** `provider.agentId` is that registration's id.
612
+ - **I3** `payTo` equals `get_agent_wallet(agentId)`.
613
+ - **V1** Clients SHOULD enforce configured verification requirements from trusted, matching, current evidence before signing.
614
+ - **X1** Extra fields go in `extensions` with reverse-DNS keys, and clients ignore unknown ones.