@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,163 @@
1
+ # ERC-8126 and ERC-8196 compatibility
2
+
3
+ **Status:** draft. Anything marked **Proposed** still has to be agreed with the team building ERC-8126 and
4
+ ERC-8196 on Stellar, which is a separate project with its own grant.
5
+
6
+ **Specs:** [ERC-8126](references/ercs/erc-8126.md) (AI Agent Verification) and
7
+ [ERC-8196](references/ercs/erc-8196.md) (AI Agent Authenticated Wallet). Provenance is in
8
+ [references/README.md](references/README.md).
9
+
10
+ ## Goal
11
+
12
+ This repository does **not** implement either ERC. The SOW also excludes "the complete ERC-8126 specification".
13
+ The goal is narrower: when Stellar implementations of these ERCs ship, they plug into what we build now without
14
+ breaking changes on either side.
15
+
16
+ ## How the layers fit
17
+
18
+ | Layer | Question it answers | Score | Built by |
19
+ | --- | --- | --- | --- |
20
+ | Stellar 8004 (ERC-8004) | Who is this agent? | – | `stellar-8004` (exists) |
21
+ | ERC-8126 | Is this agent safe? | risk 0–100, **lower is better** | parallel project |
22
+ | Provider Standard verifier (this repo) | Does this service follow the standard, take payment correctly and recover jobs? | conformance 0–100, **higher is better** | this repo |
23
+ | ERC-8196 | Is this action allowed right now? | – | parallel project (on-chain); our payment client enforces off-chain first |
24
+ | x402 | How is the service paid? | – | public `@x402/*` packages |
25
+
26
+ - **Complementary, not overlapping.** Our checks (C1–C7) test conformance, while ERC-8126 tests security risk.
27
+ They overlap in two places:
28
+ - ERC-8126 Web Application Verification touches our C1 and C6 (HTTPS, reachability).
29
+ - ERC-8126 Wallet Verification touches our C4 (the `payTo` wallet).
30
+ - **No compliance claim.** We never say our receipts are ERC-8126.
31
+
32
+ ## Ethereum to Stellar mapping
33
+
34
+ Both projects need this table. Where we pick different answers, our code and theirs stop fitting together.
35
+
36
+ | ERC concept | Stellar equivalent |
37
+ | --- | --- |
38
+ | `uint256 agentId` | Stellar 8004 `agent_id: u32` |
39
+ | `address` | strkey: `G…` account or `C…` contract |
40
+ | Chain id / EIP-155 | CAIP-2 `stellar:testnet` / `stellar:pubnet` plus the network passphrase |
41
+ | EIP-712 typed data | Soroban authorization entries for on-chain actions; SEP-53 signed messages off-chain |
42
+ | EIP-3009 gasless USDC | x402 `exact` scheme on Stellar: a signed auth entry and a fee-sponsoring facilitator |
43
+ | ERC-4337 smart account | Soroban custom account contract (`__check_auth`) |
44
+ | `keccak256` | `sha256` (native in Soroban; `BytesN<32>` in the registries) |
45
+ | Value limits in `wei` | Per-asset `i128` amounts (USDC uses 7 decimals) |
46
+ | `agentWallet` | Stellar 8004 `get_agent_wallet(agent_id)` |
47
+ | `eth_getCode` (ETV, SCV) | Soroban contract instance and Wasm hash; SAC or SEP-41 for tokens |
48
+
49
+ ETV and SCV are Ethereum/Solidity-specific. Their Stellar versions (Soroban Wasm) are for the parallel project to
50
+ define.
51
+
52
+ ## Touchpoints
53
+
54
+ ### T1. Agent identity and the registration file
55
+
56
+ - **Same starting point.** Both projects start from `agent_id`, then the registration JSON. ERC-8126 does not
57
+ allow verifying from loose parameters.
58
+ - **Proposed: `payTo` equals the on-chain `agent_wallet`.**
59
+ - ERC-8004 defines `agentWallet` as "the address where the agent receives payments".
60
+ - With this rule, ERC-8126 Wallet Verification assesses exactly the address that users pay.
61
+ - Our verifier enforces it in C2.
62
+ - **Registration file fields.** The provider's registration file lists our manifest under `services`. It keeps the
63
+ standard ERC-8004 fields (`image`, `services[].endpoint`), so an ERC-8126 resolver finds the HTTPS endpoint
64
+ without knowing our manifest format.
65
+
66
+ ### T2. Validation Registry records
67
+
68
+ The two scores run in opposite directions:
69
+ - **ERC-8004 `response`:** 0 means failed and 100 means passed, so higher is better.
70
+ - **ERC-8126 risk score:** 0 is the lowest risk, so lower is better.
71
+
72
+ Stellar's `get_summary` with an empty tag averages *all* records for an agent. One raw risk score would therefore
73
+ corrupt every average.
74
+
75
+ **Proposed:**
76
+ - `response` is always higher-is-better. ERC-8126 posts `100 − overallRiskScore`. The raw risk score and the proof
77
+ ids go in the response document.
78
+ - Each standard uses its own tag. Ours is `provider-standard-v1`; ERC-8126 uses e.g. `erc8126` or one tag per type
79
+ (`erc8126-wav` and so on).
80
+ - Response documents share an envelope:
81
+ - `standard`, `agentId`, `network`, `validator`, `checkedAt`
82
+ - `score`, `scoreDirection`
83
+ - `results[]`
84
+ - evidence links
85
+ - Hashing is shared:
86
+ - `response_hash = sha256(RFC 8785 JCS(document))`
87
+ - `request_hash = sha256(JCS(request document))`
88
+ - Both projects use the same canonicalization, so a third party can recompute either hash.
89
+
90
+ ### T3. Reading a risk score
91
+
92
+ ERC-8196 gates actions on `getLatestRiskScore(agentId)`. The payment client and Stellar Agent Search will want the
93
+ same value.
94
+
95
+ **Proposed:** the Stellar ERC-8126 project does one of two things:
96
+ - exposes `get_latest_risk_score(agent_id) -> Option<u32>` on a Soroban contract, or
97
+ - documents exactly how to derive it from the Validation Registry (validator address plus tag).
98
+
99
+ Our code reads it through one small interface (a "risk source"), so either choice works. We don't depend on their
100
+ code.
101
+
102
+ ### T4. Payment policy
103
+
104
+ This is input to the payment client design, which is still open.
105
+
106
+ ERC-8196's policy is close to our "spending controls": a per-transaction cap, a daily cap, a validity window,
107
+ allow and block lists, and a verification gate. Applied to x402 on Stellar, it has four gaps that need a shared
108
+ answer:
109
+
110
+ 1. **Limits are in native value (`wei`).** x402 pays USDC through the token contract's `transfer`, so the native
111
+ value is 0. Limits must be per asset.
112
+ 2. **`allowedContracts` would only ever contain the USDC contract.** The real payee is an argument of `transfer`.
113
+ We need payee allow and block lists.
114
+ 3. **The ERC-8126 gate checks the *acting* agent**, which is the policy's `agentId`. In our flow the risk that
115
+ matters most is the *payee* (the provider). We need a payee-side gate, e.g. a maximum provider risk or a required
116
+ `provider-standard-v1` record.
117
+ 4. **The name `minVerificationScore` is misleading.** It is actually a *maximum* risk score.
118
+
119
+ **Proposed for the policy:** whichever design we choose, the payment client's policy uses ERC-8196 field names plus
120
+ named Stellar extensions. The same policy can then be registered on an ERC-8196 smart account later.
121
+
122
+ **Proposed for the journal:** ERC-8196 requires a hash-chained audit trail (`previousHash`). The payment client
123
+ journal uses the same entry chain.
124
+
125
+ ### T5. Paying x402 services from an ERC-8196 smart account
126
+
127
+ `@x402/stellar` 2.28.0 looks compatible:
128
+ - The facilitator checks the payer's auth entries by simulation.
129
+ - It accepts any address credential, not only `G…` accounts (`validateAuthEntries`).
130
+ - The client signer has the SEP-43 shape (`address`, `signAuthEntry`).
131
+
132
+ So, in principle, a Soroban smart account whose `__check_auth` enforces an ERC-8196 policy can pay x402 services
133
+ with no provider-side change.
134
+
135
+ The facilitator imposes these constraints:
136
+ - the auth tree has no sub-invocations
137
+ - signature expiry stays within its ledger bound
138
+ - the facilitator is not in the auth entries
139
+
140
+ The policy check runs inside `__check_auth`, which is outside the auth tree, so these should hold.
141
+
142
+ **Not verified yet.** This needs a testnet spike, run jointly with the parallel project.
143
+
144
+ ## What this repository does and doesn't do
145
+
146
+ **Does** (cheap, and keeps the door open):
147
+ - C2 checks that `payTo` equals `agent_wallet` (T1).
148
+ - Validation records are higher-is-better, use their own tag, and are hashed with JCS (T2).
149
+ - A risk-source interface with no dependency on the parallel project (T3).
150
+ - Payment client policy and journal formats aligned with ERC-8196, once that design is chosen (T4).
151
+
152
+ **Doesn't do:**
153
+ - ERC-8126 checks, zero-knowledge proofs (PDV) or QCV
154
+ - an ERC-8196 contract
155
+ - block any SOW deliverable on the parallel project's timeline
156
+
157
+ ## Open questions for the parallel project
158
+
159
+ 1. The score direction and tag names in the Validation Registry (T2).
160
+ 2. An on-chain risk-score contract, or a score derived from the registry (T3)?
161
+ 3. The Stellar profiles for ETV and SCV.
162
+ 4. The policy extensions: per-asset limits, payee lists, a payee gate (T4).
163
+ 5. Which smart-account base they will build on, and when a testnet x402 spike is possible (T5).
@@ -0,0 +1,201 @@
1
+ # Job Recovery
2
+
3
+ The normative rules are in [provider-standard.md §5](provider-standard.md#5-jobs). This document explains how they
4
+ fit together, how the Provider Kit and the payment client implement them, and how the recovery demo is run.
5
+ Short version: [recovery/README.md](recovery/README.md). For AI coding agents:
6
+ [recovery/README-AI.md](recovery/README-AI.md).
7
+
8
+ **Goal:** if a connection drops after payment, the user collects the same job's result without paying again.
9
+ Nobody is charged twice for one job. A payment whose outcome is unknown is decided from the chain, never by
10
+ charging again.
11
+
12
+ ## Operations
13
+
14
+ | Operation | Request | Auth |
15
+ | --- | --- | --- |
16
+ | create-job | `POST {baseUrl}/services/{name}/jobs` | x402 payment |
17
+ | check-status | `GET {baseUrl}/jobs/{jobId}` | `Job-Token` |
18
+ | get-result | `GET {baseUrl}/jobs/{jobId}/result` | `Job-Token` |
19
+
20
+ All three answer with the same job view (§5.5): `200` once the job is `succeeded` or `failed`, otherwise `202`
21
+ with `Retry-After`, plus `Location` on create-job. The paths are fixed under `baseUrl`, so a client that kept
22
+ `baseUrl`, the job id and the token can always reach the job, even if it lost every response. check-status and
23
+ get-result never take a payment and never charge [J11].
24
+
25
+ ## Job identity and binding
26
+
27
+ 1. Before the first request, the client generates the job id (a lowercase UUID, sent as `Idempotency-Key`) and a
28
+ `Job-Token` (32 random bytes), and saves both. The payment client writes them to its journal and secret store
29
+ before it calls create-job.
30
+ 2. The provider binds the job id, permanently [J4], to:
31
+ - the binding hash, `sha256(JCS({ service, input }))`;
32
+ - `sha256(Job-Token)`. The token itself is never stored [J3].
33
+ 3. Repeated requests get these answers:
34
+
35
+ | Request | Answer |
36
+ | --- | --- |
37
+ | create-job with the same id, binding and token | The existing job. No new `402`, and a payment the request carries is not settled [J5]. |
38
+ | create-job with the same id and other input or another token | `422 idempotency_key_reused` [J6] |
39
+ | create-job, check-status or get-result while the payment is still being claimed or settled | `409 request_in_progress` with `Retry-After` [J7] |
40
+ | check-status or get-result with a wrong token, or for an unknown or expired job | `404 job_not_found`, which never says whether the job exists [J10] |
41
+
42
+ A client that sends neither header (any plain x402 client) gets a provider-generated id and token in the
43
+ create-job response [J2]. It can pay, but if that response is lost, so is the job.
44
+
45
+ ## States
46
+
47
+ ```
48
+ (new) → queued | running | payment_uncertain | failed
49
+ queued → running
50
+ running → succeeded | failed | payment_uncertain (payment_uncertain: on-success only)
51
+ payment_uncertain → queued | succeeded | failed (decided from on-chain data)
52
+ ```
53
+
54
+ | `status` | `charged` |
55
+ | --- | --- |
56
+ | `queued`, `running` | `true` for `upfront`, `false` for `on-success` |
57
+ | `succeeded` | `true` |
58
+ | `failed` | `true` for `upfront` unless `error.code` is `payment_failed`; `false` for `on-success` |
59
+ | `payment_uncertain` | `"unknown"`. The output is withheld [J14]. |
60
+
61
+ `succeeded` and `failed` are final [J12]. Finished jobs stay retrievable for `jobs.retentionSeconds` [J15]. The
62
+ kit also has an internal `pending` state: an `upfront` job whose settlement has not answered yet. Requests for it
63
+ get `409`.
64
+
65
+ ## When the user is charged
66
+
67
+ | `charge` | Order | If the work fails |
68
+ | --- | --- | --- |
69
+ | `on-success` | verify → work → settle | Not charged |
70
+ | `upfront` (default) | verify → settle → work | Charged. v1 has no refunds. |
71
+
72
+ A signed Stellar payment is valid up to an **expiration ledger**, about `maxTimeoutSeconds` (30–300) after
73
+ signing [S2]. `on-success` therefore suits work well inside that window, and the kit refuses an `on-success`
74
+ service whose `expectedDurationSeconds` is more than half of `maxTimeoutSeconds`.
75
+
76
+ The kit times each `on-success` job by its payment's expiration ledger, using the ledger clock (see
77
+ [Reconciliation](#reconciliation)). It stops 10 s before that ledger closes, to leave time to settle. A job that
78
+ can't finish in time fails with `timeout` and is not charged, even when the client sent its payment long after
79
+ signing [Q8]. Long or expensive work uses `upfront`: it is paid before it starts, and its place in the queue
80
+ survives restarts [Q4].
81
+
82
+ ## Why nobody pays twice
83
+
84
+ There are five independent guards, and each one alone stops a second charge.
85
+
86
+ 1. **The client fixes the identity before paying.** The job id, token and binding are journaled before the first
87
+ request, and `sending` is journaled and flushed before the paid request goes out. From then on the client
88
+ never signs for that job again: `pay()` refuses, and `recover()` only reads.
89
+ 2. **The provider binds the job.** One job id has one binding and one token, forever [J4]. A repeated create-job
90
+ returns the same job and never settles the payment it carries [J5].
91
+ 3. **The provider binds the payment.** The payment fingerprint is `sha256(JCS({ network, payer, nonce }))`, taken
92
+ from the payer's signed Soroban auth entry. Soroban accepts an (address, nonce) pair once, and the store
93
+ refuses a second job for one fingerprint. A signed payment buys at most one job, however it is re-wrapped
94
+ [P9].
95
+ 4. **Uncertain means don't pay.** When settlement times out, errors or answers something unexpected, the job
96
+ becomes `payment_uncertain` and is never settled again [J13]. Only a facilitator answer saying the
97
+ transaction was never submitted counts as failed.
98
+ 5. **Recovery never re-sends `PAYMENT-SIGNATURE`.** It calls check-status and get-result only.
99
+
100
+ The client checks the receipt as well. Before it shows "paid", it confirms on chain that the transaction moved
101
+ the approved amount from the payer to `payTo`, and it accepts one transaction as proof for one job only [J16].
102
+
103
+ ## Reconciliation
104
+
105
+ A `payment_uncertain` job is decided from on-chain data [J13]. In the kit, `jobs/reconcile.ts` applies the
106
+ decision and `payments/onchain.ts` does the lookup.
107
+
108
+ **What identifies the payment.** The amount and recipient can't: the same payer may pay the same provider the
109
+ same price for several jobs. Instead the kit looks for the successful transaction that carries the payer's signed
110
+ auth entry **with the job's nonce**, which the provider stored when it claimed the job. Soroban accepts that pair
111
+ once, so that transaction is the job's payment, and no other transaction can be.
112
+
113
+ **Where it looks.**
114
+
115
+ 1. The transaction hash the facilitator named, if there is one.
116
+ 2. Otherwise, or if that transaction failed (a facilitator may retry), the USDC `transfer` events from the payer
117
+ in every ledger the payment could have landed in: the 320 ledgers up to its expiration ledger. Those cover
118
+ every possible ledger, because the provider and the facilitator both refuse a signature that expires more than
119
+ `maxTimeoutSeconds` (at most 300) of ledgers after verification, and a ledger takes at least a second.
120
+
121
+ **What it decides.**
122
+
123
+ | Found | Decision | The job becomes |
124
+ | --- | --- | --- |
125
+ | A successful transaction that carries the payment, with its `transfer` event | settled | `upfront`: `queued` with the receipt, and a worker runs it. `on-success`: `succeeded` with the receipt; the output was stored before settling. |
126
+ | Nothing, after the expiration ledger closed and every ledger of the window was searched | not settled | `failed` with `payment_failed`, `charged: false` |
127
+ | Anything less | undecided | Stays `payment_uncertain` |
128
+
129
+ "Undecided" covers a payment that can still land (its expiration ledger hasn't closed), a window older than the
130
+ RPC's history, an RPC error, and a transaction that used the payment but shows no matching transfer. The job
131
+ waits. Nothing is decided from a guess.
132
+
133
+ **Who runs it.**
134
+
135
+ - The provider itself, after every recovery sweep (every 30 s), when it has a Soroban RPC: `rpc` in
136
+ `createProvider()`, or `rpcUrl` in a gateway config. Testnet defaults to the public RPC. Mainnet has no public
137
+ RPC, so set your own.
138
+ - The operator, with `provider-kit reconcile`:
139
+
140
+ ```bash
141
+ provider-kit reconcile --config provider.config.json --dry-run # what would change
142
+ provider-kit reconcile --config provider.config.json # decide
143
+ provider-kit reconcile --store ./jobs.sqlite --network testnet # a library provider's store
144
+ ```
145
+
146
+ The command reads the chain and writes only the job store. It is safe to run next to a running provider, and it
147
+ exits 1 while any job is still undecided. `--rpc <url>` uses another RPC, such as one that keeps more history.
148
+
149
+ Every update is compare-and-set from `payment_uncertain`. A late facilitator answer, another process or a second
150
+ run can't overturn a decision, and they all read the same chain.
151
+
152
+ **The ledger clock.** The same RPC gives the kit a clock on ledgers (`payments/chain.ts`): the latest ledger, its
153
+ close time, and the average ledger duration over the last 100 ledgers. The clock is used to:
154
+
155
+ - time `on-success` jobs (see [When the user is charged](#when-the-user-is-charged));
156
+ - let the recovery sweep fail an `on-success` job, uncharged, as soon as its payment's expiration ledger has
157
+ passed;
158
+ - let create-job refuse a payment that expires too far out.
159
+
160
+ When the RPC is slow or down, all three fall back to the claim time plus `maxTimeoutSeconds`, and payments keep
161
+ working.
162
+
163
+ ## Clients
164
+
165
+ The payment client (`packages/payment-client`) recovers like this:
166
+
167
+ - `pay()` journals `sending` before it sends the paid request. If that request fails in transit, the journal
168
+ marks the job `uncertain`, and `PaymentUncertainError` says to recover the job, never to pay again.
169
+ - A `409 request_in_progress` means the provider holds the payment and is still settling it. The client polls
170
+ get-result at the provider's `Retry-After` until it has a job view, for up to `followSeconds`. If settlement
171
+ takes longer, it hands the job back for `recover()`.
172
+ - `recover(jobId)` (`stellar-pay recover <job-id>`) reads the job id and token from the journal and calls
173
+ get-result. It never signs.
174
+ - A `payment_uncertain` job comes back at once. Only the provider's reconciliation decides it, so polling doesn't
175
+ help; recover the job again later.
176
+
177
+ ## Response-loss drill
178
+
179
+ The SOW recovery demo needs a response lost after a real payment. The kit has a test-only switch for this:
180
+ `loseCreateJobResponse: true`, or `DRILL_LOSE_RESPONSE=1` in the example provider. With it, the provider verifies
181
+ the payment, claims the job, settles and stores the result, then drops the create-job response. It is off by
182
+ default. Never enable it in production.
183
+
184
+ To run the demo on testnet:
185
+
186
+ ```bash
187
+ # 1. The example provider, with the drill on (after npm install && npm run build at the root)
188
+ cd examples/paid-service
189
+ DRILL_LOSE_RESPONSE=1 PAY_TO=G... npm start
190
+
191
+ # 2. Pay for a job. The response is lost after the payment.
192
+ MANIFEST=http://localhost:8787/.well-known/stellar-agent-services.json
193
+ npx stellar-pay pay $MANIFEST text-stats '{"text":"lost and found"}' --dev
194
+ # ⚠ ... The payment may have gone through: recover job <job-id>, do not pay again.
195
+
196
+ # 3. Collect the result without paying
197
+ npx stellar-pay recover <job-id> --dev
198
+ ```
199
+
200
+ The evidence is the recovered result, plus the payer's account on stellar.expert showing exactly one USDC
201
+ transfer for the job.
@@ -0,0 +1,290 @@
1
+ # Payment Client: design
2
+
3
+ > Status: **accepted** (2026-10-21). A proof of concept of the core, the local-key signer and a CLI is in
4
+ > [packages/payment-client](../packages/payment-client), and a minimal web page on the same core is in
5
+ > [apps/wallet](../apps/wallet) (2026-10-22). IMPLEMENTATION_PLAN.md §5.6 and principle 3 follow this design.
6
+
7
+ ## Who it is for
8
+
9
+ - **Developers and their agents** in Claude Code, Codex or their own code. The agent finds and pays services
10
+ on its own, inside a budget the human set.
11
+ - **Non-technical users** who open a web page, pick a service, fill in a form and pay, with no seed phrase,
12
+ CLI or browser extension required.
13
+
14
+ The aim is a product people would use after the grant, not just a demo for the SOW: **a wallet for AI
15
+ agents, with spending rules set by a human**. It is the demand side of the Provider Standard.
16
+
17
+ ## Fixed requirements
18
+
19
+ - Wallet separation: keys never touch discovery or provider code.
20
+ - Explicit approval of the service, price, recipient, asset, network and when the user is charged (`charge`).
21
+ - Spending controls: a per-payment cap and a rolling budget. A failed limit check blocks the payment.
22
+ - Challenge validation against the approved manifest (provider-standard.md [P4], [P7]).
23
+ - Durable journal before sending: job id, Job-Token and binding hash. Never re-sign the same job.
24
+ - Recovery through check-status and get-result only.
25
+ - On-chain settlement proof before showing "paid": the `transfer` event matches the approved payment
26
+ (provider-standard.md [J16]).
27
+ - No custody: we never hold a user's key on a server (SOW out of scope, and a regulatory burden).
28
+
29
+ ## Decision
30
+
31
+ ### Shape: one core, two strategies, two surfaces
32
+
33
+ ```
34
+ Web app (everyone) MCP server (agents)
35
+ browse · form · approve · find · quote · pay · status ·
36
+ budget · history · recover result · budget
37
+ │ │
38
+ └──────────┬─────────────────┘
39
+ ▼
40
+ payment-client core (TypeScript, no UI)
41
+ manifest → checkChallenge → pick method → policy + budget → approve → sign → journal → send → confirm
42
+ │ │
43
+ ▼ ▼
44
+ PaymentMethod (how a payment is made) Signer (who holds the key)
45
+ ┌──────────────────────────────┐ ┌──────────────┬──────────────────┬──────────────────┐
46
+ │ x402 exact on Stellar (USDC) │ │ local key │ Stellar Wallets │ smart account: │
47
+ │ later: other schemes/assets │ │ (dev, CI, │ Kit: Freighter, │ passkey owner + │
48
+ │ once the standard has them │ │ allowance) │ xBull, Lobstr, … │ scoped agent key │
49
+ └──────────────────────────────┘ └──────────────┴──────────────────┴──────────────────┘
50
+ ```
51
+
52
+ - **Core** (`packages/payment-client`): everything that has to be right for money to be safe. It has no UI and
53
+ no key storage, and the same code runs in Node and the browser.
54
+ - **Signers** plug in behind one interface, so the core never sees how a key is held.
55
+ - **Payment methods** plug in behind another, so a new way to pay never touches the safety rules. See
56
+ [Strategies](#strategies-signers-and-payment-methods).
57
+ - **Web app** (`apps/wallet`, static, no backend): the main UI for everyone, and what the D1 video shows. The
58
+ minimal page is built (2026-10-22): a test wallet, the form from the schema, the approval screen, spending
59
+ limits, recent payments and recovery, with the local-key signer.
60
+ - Turns each service's `inputSchema` into a form, so any provider is usable with no frontend work.
61
+ - Shows price, payee, verification badges and `charge` on one approval screen.
62
+ - Budget settings, history with explorer links, and a "recover job" button.
63
+ - **MCP server** (`packages/payment-mcp`): a separate server with its own key, apart from the read-only search
64
+ server. Inside the budget it pays without asking. Above the budget it asks for approval, through MCP
65
+ elicitation or a link to the web app.
66
+
67
+ ### Where keys live
68
+
69
+ | Key | Who holds it | Can do |
70
+ | --- | --- | --- |
71
+ | Owner key | The human: a passkey (Face ID / Touch ID) or Freighter | Anything: fund, set and revoke policies, approve large payments |
72
+ | Agent key | The MCP server or SDK, on the developer's machine | Only what the policy allows |
73
+ | — | Our servers | Nothing. There is no key on any server. |
74
+
75
+ The agent key is what lets an agent pay without a prompt, so how much damage it can do has to be limited:
76
+ - **Weeks 1–2: allowance wallet.** The agent key controls a separate `G…` account that the owner tops up with
77
+ a small amount. The client enforces the policy off chain. The most it can lose is that account's balance.
78
+ - **Target: smart account.** The owner's passkey and the agent key are both signers on one Soroban smart
79
+ account, whose `__check_auth` enforces the agent's policy on chain. Even a compromised agent machine can't
80
+ spend beyond the policy. Contract accounts hold USDC without a trustline, which removes a step for
81
+ non-technical users.
82
+
83
+ ### Strategies: signers and payment methods
84
+
85
+ **Built 2026-10-22**, with the built-ins only (`packages/payment-client/src/strategies.ts`).
86
+
87
+ The core uses the Strategy pattern on two independent axes. Other Stellar wallets and payment infrastructure
88
+ can then be plugged in later without changing the core: ours, existing projects such as Eunomia or an
89
+ ERC-8196 wallet, or new ones. Any signer works with any method that supports it.
90
+
91
+ ```ts
92
+ /** Who holds the key: the SEP-43 shape @x402/stellar signs with. */
93
+ interface Signer {
94
+ address: string; // G… or C…
95
+ signAuthEntry: SignAuthEntry;
96
+ signTransaction?: SignTransaction;
97
+ /** A policy wallet's remaining on-chain budget for `asset`, in base units. Undefined: no limit. */
98
+ budget?(network: StellarNetwork, asset: string): Promise<string | undefined>;
99
+ }
100
+
101
+ /** How an x402 challenge entry is paid, and how that payment is proven. */
102
+ interface PaymentMethod {
103
+ id: string; // "x402-exact-stellar", journaled with each payment
104
+ matches(requirements: PaymentRequirements): boolean;
105
+ createPayload(requirements: PaymentRequirements, signer: Signer): Promise<SignedPayload>;
106
+ confirm(transaction: string, expected: ExpectedPayment): Promise<Confirmation>;
107
+ }
108
+ ```
109
+
110
+ The built-ins are `localKeySigner(secret, network)` and `x402ExactStellar({ rpcUrl })`.
111
+ `new PaymentClient({ signer, methods })` uses `[x402ExactStellar()]` when `methods` is not given.
112
+
113
+ **The core keeps every safety step; adapters can't skip them.** For every signer and method:
114
+
115
+ 1. `checkChallenge()`: the challenge equals the manifest's payment option [P4].
116
+ 2. The first installed method whose `matches()` is true pays. If none matches, or `matches()` throws, the
117
+ payment is refused before anything is signed.
118
+ 3. The terms the user approves (network, asset, amount, payTo) come from the challenge entry, never from an
119
+ adapter, so no adapter can change them.
120
+ 4. The spending policy applies, and so does a policy wallet's `budget()`. A budget that is too low, can't be
121
+ read, or isn't in base units blocks the payment (fail closed). Both run again inside `pay()`.
122
+ 5. Approval, then journal `approved`.
123
+ 6. `createPayload()` signs. Only one `pay()` per job runs at a time, so a slow wallet popup can't lead to a
124
+ second signature. If the wallet does not sign, the core journals `refused`. Nothing was sent, so the user
125
+ can try again.
126
+ 7. Journal `sending` with the terms, the payer and the method id, and flush it. Only then send.
127
+ 8. The method recorded in `sending` confirms the payment against the journaled terms. If that method is not
128
+ installed, or it throws, the result is "not confirmed" with a reason, never "paid".
129
+
130
+ **Conformance checks** (`@stellar-agent-services/payment-client/conformance`). The core enforces the steps
131
+ above whatever is plugged in, so these checks cover what only an adapter can get right. They work with any
132
+ test framework.
133
+
134
+ | For | Checks |
135
+ | --- | --- |
136
+ | Signers: `signerConformance()` | Has a Stellar `G…` or `C…` address. Signs an auth entry as its own address. For a `G…` key, the signature verifies against it. `budget()`, if present, reports base units. |
137
+ | Payment methods: `paymentMethodConformance()` | Has a stable id. Matches its own entry, but not an unknown scheme, an unknown network or a malformed entry. Confirms a settled payment, but not the same transaction with another payee, amount, asset or payer. |
138
+
139
+ The built-ins pass them, and the test suite also checks that they fail broken adapters.
140
+
141
+ **Adapters and trust levels.** An adapter for another wallet or payment infrastructure lives in its own package
142
+ (`signer-<name>` or `method-<name>`). It depends on the core's types, and the core never depends on it. Users
143
+ see how far we vouch for each one:
144
+
145
+ | Level | Meaning | Shown as |
146
+ | --- | --- | --- |
147
+ | Built-in | Ours, in this package: `localKeySigner`, `x402ExactStellar` | No label |
148
+ | Verified | Passes the conformance checks, and we reviewed it | A "verified" badge |
149
+ | Community | Anyone's adapter that we have not reviewed | A warning at setup |
150
+
151
+ A wallet's own security, such as how it stores its key, stays its own responsibility. The core guarantees only
152
+ that what leaves through it is what the user approved.
153
+
154
+ How to build, test and submit an adapter, and what we and the integrator each need, is in
155
+ [client-integration/README.md](client-integration/README.md) (for AI agents:
156
+ [README-AI.md](client-integration/README-AI.md)).
157
+
158
+ **Integrations, later.** None is built yet. Each will be its own package.
159
+
160
+ | Integration | Slot | Status and what to check first |
161
+ | --- | --- | --- |
162
+ | [Stellar Wallets Kit](https://stellarwalletskit.dev): Freighter, xBull, Lobstr, Albedo, Hana, … | Signer | Week 1 spike: which wallets' `signAuthEntry` works with `@x402/stellar` |
163
+ | Passkey smart account (OpenZeppelin) | Signer, with `budget()` from its policy | Week 3 spike: a `C…` account paying through the facilitator |
164
+ | [Eunomia](https://github.com/eunomia-finance/eunomia) treasury | Signer with `budget()` if its treasury can authorize the SAC `transfer` that x402 `exact` uses; otherwise a payment method, which needs a Provider Standard change | Testnet only. Read its `packages/x402` first. |
165
+ | ERC-8196 policy wallet (a parallel project) | Signer with `budget()` | In development. Our policy and journal already use ERC-8196 field names. |
166
+ | Other x402 schemes (such as `upto`) or other assets | Payment method | Each needs a Provider Standard change and a provider that offers it |
167
+
168
+ ### Policy (ERC-8196 field names plus Stellar extensions)
169
+
170
+ | Field | Meaning | Default |
171
+ | --- | --- | --- |
172
+ | `policyId`, `ownerAddress`, `agentAddress` | ERC-8196 | — |
173
+ | `agentId` | The agent's Stellar 8004 id, if it has one | unset |
174
+ | `allowedActions` | `["x402.pay"]` | — |
175
+ | `allowedContracts` / `blockedContracts` | The asset contract (USDC SAC) | USDC only |
176
+ | `maxValuePerTx`, `maxValuePerDay` | In the asset's base units, not a native value | 0.10 / 1.00 USDC |
177
+ | `validAfter`, `validUntil` | ERC-8196 | now / +30 days |
178
+ | `minVerificationScore` | ERC-8196 name: maximum allowed provider risk score (0–100, lower is safer), from a trusted source | unset |
179
+ | `stellar.asset` | The one asset the limits apply to | USDC |
180
+ | `stellar.payees` | `{ allow?, block? }` lists of `payTo` addresses, since the real payee is an argument of `transfer` | unset |
181
+ | `stellar.minProviderLevel` | The lowest `provider-standard-v1` conformance level a payee must have | unset (pilot) |
182
+ | `stellar.autoApproveUpTo` | Payments at or below this amount, and inside the limits, need no prompt | **0: approve every payment** |
183
+
184
+ `policyHash = sha256(JCS(policy))`. Every journal entry and every approval names the policy hash it ran under.
185
+ Out of the box, every payment asks for approval, which is what the SOW requires. Paying without a prompt is
186
+ something the owner turns on.
187
+
188
+ ### Verification source
189
+
190
+ The core enforces configured `minVerificationScore` and `stellar.minProviderLevel` at quote time and again before
191
+ signing [V1]. Missing or invalid evidence blocks the payment, including when the configured limit is zero. When
192
+ both limits are unset, no verification read is needed.
193
+
194
+ `PaymentClientOptions.verificationSource` is application-trusted code implementing:
195
+
196
+ ```ts
197
+ interface VerificationSource {
198
+ read(subject: VerificationSubject): Promise<ProviderVerification | undefined>;
199
+ }
200
+ ```
201
+
202
+ The subject contains `network`, `payTo`, `manifestUrl`, `manifestSha256` (RFC 8785 canonical JSON) and optional
203
+ `agentId`. The result repeats those fields and adds RFC 3339 `checkedAt` and `expiresAt`, plus optional
204
+ `riskScore` (0–100, lower is safer) and `level` (0–3). The core rejects a different subject, a future check, expired
205
+ evidence or an invalid metric. Each configured restriction requires its own metric.
206
+
207
+ The source authenticates the evidence: trusted validator selection, registry reads, receipt hashes and the
208
+ validity period it accepts. Its network calls must have deadlines. A raw receipt, a provider's badge or a score
209
+ copied from a manifest is not an authenticated source. No built-in registry/risk-source adapter exists yet.
210
+
211
+ `minVerificationScore` preserves ERC-8196's maximum-risk meaning despite its name (erc-compatibility.md T4).
212
+ Do not compare it directly with the Provider Standard receipt's higher-is-better `score`.
213
+
214
+ The web page and CLI do not install a source. Their default policies leave both restrictions unset; importing a
215
+ policy that sets them blocks new payments until a source is installed. Recovery remains available without a
216
+ verification read, because collecting a paid result never requires another payment [J11], [J13].
217
+
218
+ ### Journal (ERC-8196 hash-chained audit trail)
219
+
220
+ - One entry per step: `intent`, `approved` or `refused` (with the policy rule that refused), `sending`,
221
+ `settled` or `uncertain`, `recovered`, `result`.
222
+ - Each entry has `seq`, `previousHash`, `policyHash`, `jobId`, `service`, `baseUrl`, `bindingHash`, `amount`,
223
+ `payTo`, the payment method and, when known, the transaction hash. `hash = sha256(JCS(entry))`.
224
+ - `sending` is written, and flushed to disk, **before** the paid request goes out. Recovery starts from the
225
+ last `sending` with no `settled` or `result` after it, and never re-signs that job.
226
+ - Job tokens are secrets, so they are stored next to the journal, not in it. The journal can then be exported
227
+ or anchored without exposing them.
228
+ - Storage is a file in Node (the `./node` entry point) and IndexedDB in the browser (`./browser`, written with
229
+ strict durability). A journal has one writer, so the web app runs in one tab at a time. Anchoring a Merkle root
230
+ on chain is later work.
231
+
232
+ ### One payment, step by step
233
+
234
+ 1. Fetch the manifest and validate it (the protocol package).
235
+ 2. Build the job: `jobId`, `Job-Token` and binding hash. Journal `intent`.
236
+ 3. Call create-job without paying and get the 402. `checkChallenge()` against the manifest, and pick the
237
+ payment method that matches it. A violation, or no matching method, refuses.
238
+ 4. Policy check, plus the wallet's own budget for a policy wallet. If either fails, journal `refused` and
239
+ stop (fail closed). If the amount is above `autoApproveUpTo`, show the approval screen. `decline()` journals
240
+ `refused` and never signs.
241
+ 5. Journal `approved`, sign, then journal `sending` and send. A busy provider (`429` or `503`, spec §5.8) answers
242
+ before it takes the payment: the client waits out `Retry-After` and sends the same signed request again, while
243
+ the payment is still valid and for at most `busyWaitSeconds`. If the provider stays busy, it journals `failed`
244
+ with `charged: false`. The unpaid request in step 3 is waited out the same way.
245
+ 6. Get the job view. Confirm the USDC `transfer` on chain before showing "paid", then journal `settled`.
246
+ 7. On `202`, or if the connection drops: check-status, then get-result, never paying again, at the pace the
247
+ provider's `Retry-After` sets. A `409 request_in_progress` means the provider holds the payment and is still
248
+ settling it: the client keeps polling, and hands the job back for `recover()` if settlement outlasts
249
+ `followSeconds`. By default a job marked `payment_uncertain` comes back at once. With
250
+ `followPaymentUncertain: true`, the client keeps reading that same job within `followSeconds`, at the
251
+ provider's `Retry-After` pace, while provider reconciliation decides its payment from the chain [J13].
252
+ It never signs or submits another payment while waiting [J11]. `provider-kit try` enables this bounded wait.
253
+
254
+ ## Sprint plan
255
+
256
+ | Week | Build | Shows |
257
+ | --- | --- | --- |
258
+ | 1 | Core (policy, journal, x402 flow, recovery skeleton), local-key signer, a minimal approval page. Spike: Stellar Wallets Kit, listing the wallets whose `signAuthEntry` works with `@x402/stellar`. | The first paid testnet transaction; the D1 video of the approval screen |
259
+ | 2 | Web app v1: form from schema, approval, budget, history. MCP server: paying inside the budget, approval above it. | Developers and non-technical users can both run the flow |
260
+ | 3 | Recovery in both surfaces. Spike: a passkey smart account paying an x402 service through the facilitator. | The recovery demo |
261
+ | 4 | Pilot. If the spike passed, non-technical pilot users get passkey wallets. | Five user flows |
262
+
263
+ The smart account is the flagship, but no SOW deliverable depends on it: the allowance wallet and Freighter
264
+ cover the sprint whatever the spike shows.
265
+
266
+ ## Risks and open points
267
+
268
+ - **Browser calls need CORS.** The web app calls providers straight from the browser. Provider Standard v1
269
+ makes this a SHOULD ([J17]) and Provider Kit turns it on by default. A provider without CORS is reachable
270
+ only through the MCP server or SDK.
271
+ - **Browser wallets:** whether each wallet's `signAuthEntry` output fits what `@x402/stellar` expects is
272
+ unverified (Week 1 spike, through Stellar Wallets Kit). Freighter is the first to test. Fallback for the D1
273
+ video: the local-key signer behind the same approval page.
274
+ - **Smart account x402 payments** are unverified (erc-compatibility.md, T5). We use an existing audited
275
+ smart-account contract, such as OpenZeppelin's Stellar smart accounts, rather than writing our own. Building
276
+ an ERC-8196 contract stays with the parallel project.
277
+ - **Moving money in and out** for non-technical users on mainnet: many exchanges can't send to a `C…` address.
278
+ On testnet we pre-fund pilot wallets.
279
+ - **Principle 3** (IMPLEMENTATION_PLAN.md) now reads: the user approves each exact payment, or the payment
280
+ fits a budget the user explicitly granted.
281
+
282
+ ## Options considered
283
+
284
+ | Option | Why it was not chosen alone |
285
+ | --- | --- |
286
+ | Node library + CLI with an encrypted keystore | Fine for developers, unusable for non-technical users. It survives as the core plus the local-key signer. |
287
+ | Keyed MCP server with elicitation | Agents only. It survives as one of the two surfaces. |
288
+ | Browser page with Freighter | Needs an extension, and every payment needs a human, so agents can't act on their own. It survives as one signer, through Stellar Wallets Kit. |
289
+ | Payment methods as separate options (`createPayload`, `confirmPayment`, as in the first PoC) | Nothing tied a method's payload to its proof, so a new method could pair them wrongly. Replaced by `PaymentMethod` on 2026-10-22. |
290
+ | Hosted wallet that holds keys | Custody, which is out of scope and a regulatory burden. |