@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.
- package/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/docs.d.ts +35 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +92 -0
- package/dist/docs.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +125 -0
- package/dist/server.js.map +1 -0
- package/package.json +40 -4
- package/repository/AGENTS.md +162 -0
- package/repository/CONTRIBUTING.md +95 -0
- package/repository/IMPLEMENTATION_PLAN.md +560 -0
- package/repository/README.md +80 -0
- package/repository/SECURITY.md +22 -0
- package/repository/apps/wallet/README.md +78 -0
- package/repository/docs/client-integration/README-AI.md +495 -0
- package/repository/docs/client-integration/README.md +138 -0
- package/repository/docs/dx-improvements.md +670 -0
- package/repository/docs/erc-compatibility.md +163 -0
- package/repository/docs/job-recovery.md +201 -0
- package/repository/docs/payment-client.md +290 -0
- package/repository/docs/provider-setup-guide.md +348 -0
- package/repository/docs/provider-standard.md +728 -0
- package/repository/docs/recovery/README-AI.md +288 -0
- package/repository/docs/recovery/README.md +119 -0
- package/repository/docs/sow-closeout.md +67 -0
- package/repository/docs/standard/README-AI.md +614 -0
- package/repository/docs/standard/README.md +108 -0
- package/repository/docs/verification.md +504 -0
- package/repository/examples/paid-service/README.md +33 -0
- package/repository/examples/python-upstream/README.md +44 -0
- package/repository/llms.txt +65 -0
- package/repository/packages/create-stellar-service/README.md +143 -0
- package/repository/packages/dev-mcp/README.md +51 -0
- package/repository/packages/payment-client/README.md +98 -0
- package/repository/packages/protocol/README.md +15 -0
- package/repository/packages/protocol/schemas/manifest.schema.json +272 -0
- package/repository/packages/protocol/test-vectors/README.md +45 -0
- package/repository/packages/provider-kit/README.md +509 -0
- package/repository/packages/provider-kit/schemas/gateway-config.schema.json +140 -0
- package/repository/packages/verifier/README.md +131 -0
- 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. |
|