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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) 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 +548 -0
  19. package/repository/README.md +78 -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 +487 -0
  23. package/repository/docs/client-integration/README.md +137 -0
  24. package/repository/docs/dx-improvements.md +666 -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 +258 -0
  28. package/repository/docs/provider-setup-guide.md +348 -0
  29. package/repository/docs/provider-standard.md +724 -0
  30. package/repository/docs/recovery/README-AI.md +288 -0
  31. package/repository/docs/recovery/README.md +119 -0
  32. package/repository/docs/standard/README-AI.md +611 -0
  33. package/repository/docs/standard/README.md +108 -0
  34. package/repository/docs/verification.md +502 -0
  35. package/repository/examples/paid-service/README.md +24 -0
  36. package/repository/examples/python-upstream/README.md +44 -0
  37. package/repository/llms.txt +64 -0
  38. package/repository/packages/create-stellar-service/README.md +143 -0
  39. package/repository/packages/dev-mcp/README.md +51 -0
  40. package/repository/packages/payment-client/README.md +75 -0
  41. package/repository/packages/protocol/README.md +15 -0
  42. package/repository/packages/protocol/schemas/manifest.schema.json +272 -0
  43. package/repository/packages/protocol/test-vectors/README.md +45 -0
  44. package/repository/packages/provider-kit/README.md +507 -0
  45. package/repository/packages/provider-kit/schemas/gateway-config.schema.json +140 -0
  46. package/repository/packages/verifier/README.md +127 -0
  47. package/repository/plugins/stellar-agent-services/README.md +31 -0
@@ -0,0 +1,487 @@
1
+ # Payment Client Integrations: implementation reference for AI coding agents
2
+
3
+ Use this file to build, review or submit one of three things:
4
+
5
+ 1. a **Signer** adapter: a wallet plugged into `packages/payment-client`;
6
+ 2. a **PaymentMethod** adapter: a way to pay and prove a payment, plugged into the same core;
7
+ 3. a **native client**: another project's own payment client that pays Provider Standard v1 services directly.
8
+
9
+ - **Sources:** the interfaces are in [strategies.ts](../../packages/payment-client/src/strategies.ts), the core flow
10
+ in [client.ts](../../packages/payment-client/src/client.ts), the design in
11
+ [payment-client.md](../payment-client.md#strategies-signers-and-payment-methods), and the wire protocol in
12
+ [provider-standard.md](../provider-standard.md). The spec wins on the wire protocol. The code is the current
13
+ behavior; if this file disagrees with it, report the mismatch.
14
+ - **Rule ids in this file:** `CG` core guarantees, `SG` signer rules, `PM` payment method rules, `AD` adapter
15
+ package rules. They are integration rules, not part of Provider Standard v1. Spec rules keep their own ids
16
+ (`[P7]`, `[J16]`).
17
+ - **Human summary:** [README.md](README.md).
18
+ - **State (2026-11-02):** only the built-ins exist (`localKeySigner`, `x402ExactStellar`). No third-party adapter exists
19
+ yet. The package is on npm as `@stellar-agent-services/payment-client`, from 0.1.0.
20
+
21
+ ## 0. Rules for the agent using this file
22
+
23
+ 1. Pick one path in §1. Don't edit `packages/payment-client/src` to make an adapter work. If the core lacks
24
+ something, stop and report it as an issue; don't work around it inside the adapter.
25
+ 2. An adapter never touches the journal, the policy, the job token or provider HTTP calls. It signs, or it
26
+ confirms. Nothing else.
27
+ 3. Fail closed. A signer or `createPayload()` that can't do exactly what was asked throws. `confirm()` that
28
+ can't tell returns `{ confirmed: false, reason }`. Never return a placeholder signature or `confirmed: true`.
29
+ 4. No secret keys in source, fixtures, logs or CI output. Tests use `Keypair.random()`; testnet keys come from the
30
+ environment.
31
+ 5. Tests never touch the network, except the opt-in testnet runs in §10.
32
+ 6. Never call an adapter "verified". Only maintainers grant that level (§12).
33
+
34
+ ## 1. Choose a path
35
+
36
+ | Situation | Path | Slot | Standard change first? |
37
+ | --- | --- | --- | --- |
38
+ | A wallet holding a `G…` key (browser extension, mobile, HSM, MPC) that signs SEP-43 `signAuthEntry` | Signer adapter | `Signer` | No |
39
+ | A wallet that also enforces spending limits on chain (ERC-8196 policy wallet, treasury) | Signer adapter with `budget()` | `Signer` | No |
40
+ | A contract account (`C…`) whose `__check_auth` expects its own signature format (passkey/WebAuthn smart account) | Payment method for scheme `exact` that builds its own auth entries, plus a signer for the account | `PaymentMethod` + `Signer` | No |
41
+ | A new scheme, asset, network or protocol (`upto`, MPP, another token) | Payment method adapter | `PaymentMethod` | **Yes** (spec §11, `[P1]`). Providers can't offer it before that. |
42
+ | Your own client, in any language | Native client | none | No. Follow §11. |
43
+
44
+ **Why `C…` accounts need their own method.** `x402ExactStellar()` uses `@x402/stellar` 2.28's
45
+ `ExactStellarScheme`, which signs through `AssembledTransaction.signAuthEntries`. That path turns the signer's
46
+ answer into a bare signature (`Buffer.from(signedAuthEntry, "base64")`), and `authorizeEntry` then verifies it with
47
+ `Keypair.fromPublicKey(address)`. That works only for `G…` addresses. A custom account needs `authorizeEntry` to
48
+ receive `{ signatureScVal }` in the format its `__check_auth` expects, and only a method that builds the auth entry
49
+ itself can supply it. (The design doc's smart-account row, and erc-compatibility.md T5, are still unverified.)
50
+
51
+ ## 2. File map
52
+
53
+ | File | Role |
54
+ | --- | --- |
55
+ | `packages/payment-client/src/strategies.ts` | `Signer`, `PaymentMethod`, `SignedPayload`, `ExpectedPayment`, `Confirmation` |
56
+ | `packages/payment-client/src/client.ts` | `PaymentClient`: the flow that calls the strategies (`quote`, `pay`, `payOnce`, `confirm`, `methodFor`, `checkLimits`) |
57
+ | `packages/payment-client/src/local-key-signer.ts` | Built-in signer: `createEd25519Signer` from `@x402/stellar` |
58
+ | `packages/payment-client/src/x402-exact-stellar.ts` | Built-in method, id `x402-exact-stellar` |
59
+ | `packages/payment-client/src/onchain.ts` | `rpcConfirmPayment`, `checkTransfer`, `TransactionReader`. Reuse them in your `confirm()`. |
60
+ | `packages/payment-client/src/conformance.ts` | `signerConformance`, `paymentMethodConformance` (entry point `./conformance`) |
61
+ | `packages/payment-client/test/conformance.test.ts` | How the built-ins run conformance, and the broken adapters it must fail |
62
+ | `packages/payment-client/test/support/chain.ts` | `fakeChain`, `transferEvent`. Not exported: copy them (§8.4). |
63
+ | `packages/payment-client/test/flow.test.ts`, `describe("strategies")` | The core's behavior around strategies |
64
+ | `packages/provider-kit/src/payments/payload-checks.ts` | What a provider checks in your payload before its facilitator sees it |
65
+ | `examples/paid-service` | The provider to pay in the testnet runs (§10) |
66
+ | `apps/wallet/src/lib/agent-wallet.svelte.ts` | Where the web app builds its `PaymentClient`; swap the signer here to try a browser wallet locally |
67
+
68
+ ## 3. Interfaces
69
+
70
+ From `strategies.ts`, with the SEP-43 types expanded (`@stellar/stellar-sdk/contract`):
71
+
72
+ ```ts
73
+ /** Who holds the key: ClientStellarSigner from @x402/stellar, plus budget(). */
74
+ interface Signer {
75
+ address: string; // G… or C…
76
+ signAuthEntry: SignAuthEntry;
77
+ signTransaction?: SignTransaction; // never called by the core or by x402 exact
78
+ /** A wallet's own on-chain limit for `asset`, in base units. Undefined: no limit. */
79
+ budget?(network: StellarNetwork, asset: string): Promise<string | undefined>;
80
+ }
81
+
82
+ type SignAuthEntry = (
83
+ authEntry: string, // base64 xdr.HashIdPreimage (envelopeTypeSorobanAuthorization)
84
+ opts?: { networkPassphrase?: string; address?: string },
85
+ ) => Promise<{ signedAuthEntry: string; signerAddress?: string } & { error?: { message: string; code: number } }>;
86
+
87
+ /** How an x402 challenge entry is paid, and how that payment is proven. */
88
+ interface PaymentMethod {
89
+ readonly id: string; // journaled with each payment
90
+ matches(requirements: PaymentRequirements): boolean;
91
+ createPayload(requirements: PaymentRequirements, signer: Signer): Promise<SignedPayload>;
92
+ confirm(transaction: string, expected: ExpectedPayment): Promise<Confirmation>;
93
+ }
94
+
95
+ type SignedPayload = Pick<PaymentPayload, "x402Version" | "payload">; // @x402/core/types
96
+ interface ExpectedPayment { network: StellarNetwork; asset: string; amount: string; payTo: string; payer: string }
97
+ interface Confirmation { confirmed: boolean; reason?: string }
98
+ ```
99
+
100
+ Facts the types don't tell you:
101
+
102
+ - The SDK calls `signAuthEntry(preimage, { address })` **without** `networkPassphrase`. Take the network when the
103
+ signer is built. The preimage already commits to the network id.
104
+ - `signedAuthEntry` is the base64 raw 64-byte ed25519 signature over `sha256(preimage bytes)`, not a signed XDR
105
+ entry.
106
+ - A returned `error` makes the SDK throw (`handleWalletError`). Either way the core journals `refused`.
107
+ - `budget()` and `ExpectedPayment.amount` are base units of the asset (USDC: 7 decimals), matching `^\d+$`.
108
+ - `requirements` is the challenge's single `accepts` entry: `scheme`, `network`, `asset`, `amount`, `payTo`,
109
+ `maxTimeoutSeconds`, `extra.areFeesSponsored`.
110
+ - The core sends `{ ...signed, x402Version: 2, resource, accepted }`. It sets `resource` and `accepted` itself.
111
+ Any other key you return passes through to the provider unchecked.
112
+ - `confirm(transaction, …)` gets the hash from the provider's job view (`job.payment.transaction`). It is
113
+ untrusted input.
114
+ - `PaymentClientOptions.methods`: the first method whose `matches()` is true wins. The default is
115
+ `[x402ExactStellar({ rpcUrl })]`. An empty list or duplicate ids throw a `TypeError` in the constructor.
116
+
117
+ ## 4. What the core enforces (CG)
118
+
119
+ These hold for every adapter. Don't re-implement them in an adapter.
120
+
121
+ | Id | Guarantee | Code (`client.ts`) | Test (`flow.test.ts`) |
122
+ | --- | --- | --- | --- |
123
+ | CG1 | The challenge equals the approved manifest's payment option `[P4]`, `[P7]`. Any violation blocks `pay()`. | `quote()`: `checkChallenge` | "refuses to sign a challenge that differs from the manifest" |
124
+ | CG2 | The first installed method whose `matches()` is true pays. A throw counts as false. No match refuses before signing. | `methodFor()` | "refuses before signing when no installed method pays the challenge" |
125
+ | CG3 | The approved terms (network, asset, amount, payTo) come from the challenge entry, never from an adapter. | `pick()` | "pays with the first installed method that matches, and journals which one" |
126
+ | CG4 | Spending policy plus `signer.budget()`, at `quote()` and again in `pay()`. A budget that throws, isn't base units or is below the amount blocks the payment (`wallet.budget`). | `checkLimits()` | "re-checks a policy wallet's on-chain budget when paying", "blocks the payment when the wallet's budget …" |
127
+ | CG5 | Approval, or auto-approval within `autoApproveUpTo`, comes before `createPayload()`. Journals `approved`. | `payOnce()` | "pays nothing when the user declines", "declines without signing, even inside the auto-approve budget" |
128
+ | CG6 | `createPayload()` runs at most once per job: an in-process `paying` set, plus the journal's `sending` entry. | `pay()` | "signs once when pay() runs twice at the same time" |
129
+ | CG7 | `createPayload()` throws: journal `refused`, nothing sent, the same quote can be paid again. | `payOnce()` | "sends nothing when the wallet does not sign, and lets the user try again" |
130
+ | CG8 | `sending` with `{ network, asset, amount, payTo, payer: signer.address, method: id }` is durable before the paid request leaves. | `payOnce()` | same as CG3 |
131
+ | CG9 | The method named in `sending` confirms, against the journaled terms, never the receipt. A transaction that already confirmed another job, a missing method or a method that throws gives "not confirmed" with a reason `[J16]`. | `confirm()` | "confirms against the approved payment, not the provider's receipt", "does not confirm a job whose receipt reuses another job's transaction", "does not confirm a recovered payment whose method is no longer installed", "does not confirm a payment whose method throws while checking it" |
132
+ | CG10 | `recover()` never calls `createPayload()`. | `recover()` | "recovers a lost response without paying again" |
133
+
134
+ Journal on the happy path: `intent, approved, sending, settled, result`. When the wallet doesn't sign:
135
+ `intent, approved, refused`.
136
+
137
+ **What the core does not enforce.** This is why the `SG` and `PM` rules exist.
138
+
139
+ - **It doesn't inspect what is signed.** `createPayload()` receives the whole `Signer`, and the core never decodes
140
+ its result. A method that authorizes another amount or payee is refused by an honest provider
141
+ (`payload-checks.ts`) and by its facilitator. But by then a signed authorization has left the process, and anyone
142
+ holding it can submit it until its expiration ledger.
143
+ - **It doesn't bind the policy to the signer.** `policy.agentAddress` is never compared with `signer.address`.
144
+ - **It reads `signer.address` again after signing**, to journal `payer`. If the address changed during signing,
145
+ confirmation fails. The user sees "not confirmed"; nothing is charged twice.
146
+ - **It doesn't time out `createPayload()`.** A wallet popup left open keeps that job's `pay()` pending. A signature
147
+ produced after `maxTimeoutSeconds` is refused by the provider (`402`, journaled `failed`, `charged: false`).
148
+
149
+ ## 5. Signer rules (SG)
150
+
151
+ | Id | Rule | Checked by |
152
+ | --- | --- | --- |
153
+ | SG1 | MUST: `address` is a valid `G…` or `C…` strkey (never `M…`), constant for the instance's lifetime. A wallet account switch means a new `Signer` and a new `PaymentClient`. | `signerConformance`: "has a Stellar account (G...) or contract (C...) address". Constancy: review. |
154
+ | SG2 | MUST sign only as `address`. A returned `signerAddress` equals `address`. For `G…`, the signature verifies against `address` over `sha256(preimage)`. | "signs an auth entry as its own address", "its signature verifies against its address" |
155
+ | SG3 | MUST sign only the preimage it is given. MUST NOT build, submit or broadcast a transaction (never `submit: true`), and MUST NOT sign anything on its own initiative. | Review |
156
+ | SG4 | MUST take the network when it is built, not from `opts`. SHOULD throw for a preimage whose `networkId` is not `sha256(its passphrase)`. | Review |
157
+ | SG5 | On cancel, refusal or a wallet error, MUST throw or return `error`. MUST NOT return an empty or placeholder signature. | Review. "signs an auth entry as its own address" rejects an empty one. |
158
+ | SG6 | `budget()`: only for a wallet that enforces the limit itself, on chain. Returns what `asset` on `network` can still pay, in base units, or `undefined` for "no limit". MUST be read-only: no prompt, no signature, no state change. Throws when it can't tell. | "reports its budget in base units". Read-only: review. |
159
+ | SG7 | `budget()` SHOULD answer within about 2 s. It runs on every `quote()` and `pay()`. | Review |
160
+ | SG8 | Omit `signTransaction` unless your wallet needs it elsewhere. The core never calls it. | — |
161
+ | SG9 | The adapter card declares where the key lives. A signer whose key a server holds for the user can't be Verified. | Review |
162
+ | SG10 | A signer that prompts a human SHOULD show the asset, amount, payee and network decoded from the preimage. | Review |
163
+
164
+ ## 6. Payment method rules (PM)
165
+
166
+ | Id | Rule | Checked by |
167
+ | --- | --- | --- |
168
+ | PM1 | `id` matches `^[a-z0-9]+(-[a-z0-9]+)*$`, is unique, and is never renamed or reused. Name it after what it pays and who built it: `x402-exact-stellar-<name>`. Recovery finds the method by this id. | `paymentMethodConformance`: "has a stable id". Uniqueness: review, and the core's constructor. |
169
+ | PM2 | `matches()` is synchronous and pure: no I/O, no throw. True only for an entry it can pay: known scheme, network and asset, and a well-formed entry. | "matches the challenge entry it pays", "does not match an unknown scheme", "… an unknown network", "… a malformed entry" |
170
+ | PM3 | `createPayload()` MUST authorize exactly one payment: `asset.transfer(signer.address, payTo, amount)` on `network`, with no sub-invocations and no other auth entry for the payer. The signature expires no later than the current ledger + `ceil(maxTimeoutSeconds / ledger seconds)`. | **Not checked by conformance yet.** Review, and the self-test in §8.5. |
171
+ | PM4 | `createPayload()` MUST NOT submit, broadcast or settle the payment, or send the payload anywhere. It returns it. Only the provider's facilitator settles. If it throws, no signed authorization may have left the process. | Review |
172
+ | PM5 | `createPayload()` signs only with the `signer` argument, once per auth entry, and never caches a payload. Its only network calls are Soroban RPC reads and simulation. | Review |
173
+ | PM6 | It returns only `{ x402Version: 2, payload }`. For `exact` on Stellar, `payload` is `{ transaction: <base64 transaction XDR> }`, which must pass `payload-checks.ts`. | Review, and the testnet runs |
174
+ | PM7 | `confirm()` reads the chain only. It checks against `expected`, never the provider's receipt: the transaction succeeded and has exactly one `transfer` of `expected.asset` from `expected.payer`, to `expected.payTo`, for `expected.amount`. Reuse `rpcConfirmPayment` or `checkTransfer`. | "confirms the settled payment", "does not confirm a payment to another payee", "… another amount", "… another asset", "… a payment from another payer" |
175
+ | PM8 | `confirm()` returns `{ confirmed: false, reason }` with a non-empty reason when the transaction is missing, pending or the RPC fails. It is idempotent: recovery may call it again later. | The non-empty reason is asserted. Idempotency: review. |
176
+ | PM9 | `confirm()` keeps working for payments already journaled under its id. Changing what it accepts is a major version. | Review |
177
+ | PM10 | A method outside v1 (a scheme other than `exact`, another asset, network or protocol) needs a Provider Standard change first (spec §9, §11). Until then it ships as Community. | Maintainers |
178
+ | PM11 | A method for `C…` accounts builds auth entries with `authorizeEntry(entry, callback, expiration, passphrase)`, its callback returning `{ signatureScVal }`. It matches only when configured for that account. Install it **instead of** or **before** `x402ExactStellar()`, since the first match wins. | Review, and the testnet runs |
179
+
180
+ ## 7. Package rules (AD)
181
+
182
+ | Id | Rule |
183
+ | --- | --- |
184
+ | AD1 | Its own package, named `signer-<name>` or `method-<name>` (under your own npm scope). |
185
+ | AD2 | `@stellar-agent-services/payment-client` is a `peerDependency` (and a `devDependency` for tests). Import only its types, `rpcConfirmPayment` / `checkTransfer`, and `./conformance`. Pin `@x402/core` and `@x402/stellar` 2.28.0 and `@stellar/stellar-sdk` 16.4.1 as the core does. The core never imports the adapter. |
186
+ | AD3 | ESM. Runs where it says it does: Node ≥ 22.13, the browser, or both. A browser adapter uses no Node-only API (`Buffer`, `fs`). `./conformance` itself uses `Buffer`, so run it in Node. |
187
+ | AD4 | CI runs the conformance checks (§8.4) on every change, and they all pass. |
188
+ | AD5 | No keys, preimages, signatures, payloads or job tokens in logs, error messages, analytics or telemetry. Errors name the cause, not the data. |
189
+ | AD6 | Semver. Changing the method `id`, or what the address, `budget()` or `confirm()` means, is a major version. |
190
+ | AD7 | Public source under an open-source license (MIT or Apache-2.0 preferred), and a security contact. |
191
+ | AD8 | The package README contains the adapter card (§9). |
192
+
193
+ ## 8. Templates
194
+
195
+ ### 8.1 Signer for a SEP-43 wallet
196
+
197
+ ```ts
198
+ import type { StellarNetwork } from "@stellar-agent-services/protocol";
199
+ import type { Signer } from "@stellar-agent-services/payment-client";
200
+ import { getNetworkPassphrase } from "@x402/stellar";
201
+
202
+ /** The SEP-43 subset of your wallet's API. Replace it with the real one. */
203
+ export interface WalletApi {
204
+ getAddress(): Promise<{ address: string }>;
205
+ signAuthEntry(
206
+ authEntry: string,
207
+ opts: { networkPassphrase: string; address: string },
208
+ ): Promise<{ signedAuthEntry: string; signerAddress?: string; error?: { message: string; code: number } }>;
209
+ }
210
+
211
+ export async function walletSigner(wallet: WalletApi, network: StellarNetwork): Promise<Signer> {
212
+ const { address } = await wallet.getAddress(); // read once [SG1]
213
+ const networkPassphrase = getNetworkPassphrase(network); // from construction, not opts [SG4]
214
+ return {
215
+ address,
216
+ async signAuthEntry(authEntry) {
217
+ const result = await wallet.signAuthEntry(authEntry, { networkPassphrase, address });
218
+ if (result.error !== undefined) throw new Error(`the wallet did not sign: ${result.error.message}`); // [SG5]
219
+ if (result.signedAuthEntry === "") throw new Error("the wallet returned no signature");
220
+ if (result.signerAddress !== undefined && result.signerAddress !== address) {
221
+ throw new Error("the wallet signed with another account"); // [SG2]
222
+ }
223
+ return { signedAuthEntry: result.signedAuthEntry, signerAddress: address };
224
+ },
225
+ };
226
+ }
227
+ ```
228
+
229
+ ### 8.2 A wallet with an on-chain budget
230
+
231
+ ```ts
232
+ // Inside the object walletSigner returns. `policyWallet.remaining` is your wallet's own read-only call.
233
+ async budget(asked: StellarNetwork, asset: string) {
234
+ if (asked !== network) throw new Error(`this wallet is on ${network}, not ${asked}`);
235
+ const remaining: bigint | null = await policyWallet.remaining(address, asset); // no prompt, no signature [SG6]
236
+ return remaining === null ? undefined : remaining.toString(); // base units
237
+ },
238
+ ```
239
+
240
+ ### 8.3 Payment method
241
+
242
+ ```ts
243
+ import { rpcConfirmPayment, type PaymentMethod, type TransactionReader } from "@stellar-agent-services/payment-client";
244
+
245
+ export const METHOD_ID = "x402-exact-stellar-example"; // [PM1]
246
+
247
+ export function exampleMethod(options: { rpcUrl?: string; server?: TransactionReader } = {}): PaymentMethod {
248
+ return {
249
+ id: METHOD_ID,
250
+ // Pure and total, including for {} [PM2]. Add your own asset and account conditions.
251
+ matches: (r) => r?.scheme === "exact" && r.network === "stellar:testnet" && typeof r.payTo === "string" && typeof r.amount === "string",
252
+ async createPayload(requirements, signer) {
253
+ // Build ONE asset.transfer(signer.address, payTo, amount) invocation and simulate it. Authorize only the
254
+ // payer's auth entry, expiring within maxTimeoutSeconds. Return { x402Version: 2, payload: { transaction } }.
255
+ // Never submit it [PM3]–[PM6].
256
+ throw new Error("not implemented");
257
+ },
258
+ confirm: rpcConfirmPayment(options), // the on-chain transfer check [PM7]; `server` lets tests pass a fake chain
259
+ };
260
+ }
261
+ ```
262
+
263
+ Accept a `server?: TransactionReader` option, as `x402ExactStellar` does, so conformance runs offline.
264
+
265
+ ### 8.4 Conformance tests
266
+
267
+ ```ts
268
+ import { USDC_ASSET } from "@stellar-agent-services/protocol";
269
+ import type { ExpectedPayment } from "@stellar-agent-services/payment-client";
270
+ import { paymentMethodConformance, signerConformance } from "@stellar-agent-services/payment-client/conformance";
271
+ import { Keypair } from "@stellar/stellar-sdk";
272
+ import type { PaymentRequirements } from "@x402/core/types";
273
+ import { describe, it } from "vitest";
274
+ import { fakeChain, transferEvent } from "./support/chain.js"; // copied from payment-client/test/support/chain.ts
275
+
276
+ const network = "stellar:testnet";
277
+ // A test double of your wallet that signs with a real key, or the real wallet in a browser test runner.
278
+ const signer = await walletSigner(testWallet(), network);
279
+
280
+ describe("signer conformance", () => {
281
+ for (const check of signerConformance(signer, { network })) it(check.name, check.run);
282
+ });
283
+
284
+ describe("payment method conformance", () => {
285
+ const payer = Keypair.random().publicKey();
286
+ const payTo = Keypair.random().publicKey();
287
+ const expected: ExpectedPayment = { network, asset: USDC_ASSET[network], amount: "100000", payTo, payer };
288
+ const transaction = "ab".repeat(32);
289
+ const server = fakeChain({ [transaction]: [transferEvent({ asset: expected.asset, from: payer, to: payTo })] });
290
+ const requirements = {
291
+ scheme: "exact", network, asset: expected.asset, amount: expected.amount, payTo,
292
+ maxTimeoutSeconds: 60, extra: { areFeesSponsored: true },
293
+ } as PaymentRequirements;
294
+ for (const check of paymentMethodConformance(exampleMethod({ server }), { requirements, settled: { transaction, expected } })) {
295
+ it(check.name, check.run);
296
+ }
297
+ });
298
+ ```
299
+
300
+ `transferEvent` writes an amount of `100000` by default, so keep `expected.amount` at `"100000"` or pass `data`.
301
+
302
+ ### 8.5 Self-test for what a method signs (PM3)
303
+
304
+ Conformance doesn't call `createPayload()` yet. Until it does, wrap the signer, record every preimage, run
305
+ `createPayload()` against testnet with a funded payer (simulation needs the USDC balance), and check each preimage:
306
+
307
+ ```ts
308
+ import { Address, hash, scValToNative, xdr } from "@stellar/stellar-sdk";
309
+ import { getNetworkPassphrase } from "@x402/stellar";
310
+
311
+ function assertApprovedTransfer(preimage: string, r: PaymentRequirements, payer: string): void {
312
+ const auth = xdr.HashIdPreimage.fromXDR(preimage, "base64").sorobanAuthorization();
313
+ if (!auth.networkId().equals(hash(Buffer.from(getNetworkPassphrase(r.network))))) throw new Error("wrong network");
314
+ const invocation = auth.invocation();
315
+ if (invocation.subInvocations().length !== 0) throw new Error("sub-invocations");
316
+ const call = invocation.function().contractFn();
317
+ const [from, to, amount] = call.args().map((arg) => scValToNative(arg) as unknown);
318
+ if (Address.fromScAddress(call.contractAddress()).toString() !== r.asset) throw new Error("wrong asset");
319
+ if (call.functionName().toString() !== "transfer") throw new Error("not a transfer");
320
+ if (from !== payer || to !== r.payTo || amount !== BigInt(r.amount)) throw new Error("wrong transfer");
321
+ }
322
+ ```
323
+
324
+ ## 9. Adapter card
325
+
326
+ Put this under `## Adapter card` in the package README. Every field is required; write `none` where it applies.
327
+
328
+ ```yaml
329
+ name: Example Wallet signer
330
+ package: "@example/signer-example" # npm name, or a repo path until published
331
+ version: 0.1.0
332
+ slot: signer # signer | payment-method
333
+ methodId: none # payment methods: the journaled id
334
+ addresses: [G] # G, C
335
+ networks: [stellar:testnet] # stellar:testnet, stellar:pubnet
336
+ budget: none # none | on-chain: <the contract or mechanism that enforces it>
337
+ humanPrompt: per-payment # never | per-payment | above-a-limit
338
+ keyCustody: browser extension on the user's device # where the key lives; "server" can't be Verified
339
+ signs: one SAC transfer(payer, payTo, amount) auth entry per payment
340
+ submits: never
341
+ dependencies: ["@stellar/stellar-sdk@16.4.1", "@x402/stellar@2.28.0"]
342
+ license: MIT
343
+ source: https://github.com/example/signer-example
344
+ security: security@example.com
345
+ audits: none
346
+ conformance: <link to a passing CI run>
347
+ testnet:
348
+ paid: <transaction hash>
349
+ recovered: <transaction hash>
350
+ ```
351
+
352
+ ## 10. Testnet runs (evidence for Verified)
353
+
354
+ Set up a provider wallet, a funded payer and the example service, as in the
355
+ [payment-client README](../../packages/payment-client/README.md#try-it-on-testnet):
356
+
357
+ ```bash
358
+ npm install && npm run build
359
+ STELLAR_PAY_HOME=~/.stellar-pay-provider npx stellar-pay wallet create
360
+ STELLAR_PAY_HOME=~/.stellar-pay-provider npx stellar-pay wallet fund --usdc 0
361
+ PAY_TO=G... node examples/paid-service/dist/server.js
362
+ ```
363
+
364
+ The CLI only uses the built-in signer, so pay with your adapter from a short script:
365
+
366
+ ```ts
367
+ import { defaultPolicy, Journal, memoryJournalStorage, memorySecretStore, PaymentClient } from "@stellar-agent-services/payment-client";
368
+
369
+ const signer = await walletSigner(wallet, "stellar:testnet");
370
+ const client = new PaymentClient({
371
+ signer,
372
+ methods: [exampleMethod()], // omit for the default x402ExactStellar()
373
+ policy: defaultPolicy(signer.address, "stellar:testnet"),
374
+ journal: await Journal.open(memoryJournalStorage()),
375
+ secrets: memorySecretStore(),
376
+ allowLocalhost: true,
377
+ });
378
+ const manifest = "http://localhost:8787/.well-known/stellar-agent-services.json";
379
+ const quote = await client.quote(manifest, "text-stats", { text: "Hello agents." });
380
+ if (!client.payable(quote)) throw new Error(quote.challengeViolations.concat(quote.policyViolations.map((v) => v.message)).join("; "));
381
+ const result = await client.pay(quote, { approved: true });
382
+ console.log(result.confirmed, result.job.payment?.transaction); // must be true, with a hash
383
+ ```
384
+
385
+ 1. **Paid job:** `result.confirmed` is `true`. The explorer shows one USDC `transfer` from the payer to `PAY_TO`
386
+ for the price.
387
+ 2. **Recovery drill:** restart the provider with `DRILL_LOSE_RESPONSE=1 REPORT_DELAY_SECONDS=8` and pay
388
+ `delayed-report` with `{ "topic": "…" }`. `pay()` throws `PaymentUncertainError`; call
389
+ `client.recover(error.jobId)`. It must return `confirmed: true`, and the explorer must show exactly one transfer
390
+ for that job.
391
+
392
+ For a browser wallet, run the same flow in [apps/wallet](../../apps/wallet) locally by swapping `localKeySigner`
393
+ in `agent-wallet.svelte.ts` for your signer. Don't commit that change.
394
+
395
+ ## 11. Native clients
396
+
397
+ A native client implements the client side of the spec itself. It gets none of the CG guarantees for free, so it
398
+ must provide them. The rules are in [standard/README-AI.md §14](../standard/README-AI.md#14-client-rules-agents-wallets-apps):
399
+
400
+ | Must | Rules |
401
+ | --- | --- |
402
+ | Fetch the manifest without redirects, at most 256 KiB, I-JSON. Reject another major `standard`. Never fetch remote schemas. | `[M3]`, `[M8]`, `[S5]` |
403
+ | Generate and persist the job id and a 32-byte token before the first request. Send both. | `[J1]` |
404
+ | Compare the challenge with the approved manifest. Refuse to sign on any difference. | `[P4]`, `[P7]` |
405
+ | Show price, payee, network and `charge` before approval. | `[S3]` |
406
+ | Sign once per job. Persist "sending" before the paid request. Never sign again for that job. | `[P9]`; CG6–CG8 |
407
+ | `429` / `503`: wait `Retry-After` plus jitter and resend the identical request, with the same `PAYMENT-SIGNATURE`. Stop before the payment expires. | `[Q8]`, `[S2]` |
408
+ | `202` / `409`: poll no faster than `Retry-After`. HTTP timeout above 30 s. | `[Q9]`, `[J7]`, `[J9]` |
409
+ | `payment_uncertain`: stop polling and tell the user not to pay again. | `[J13]` |
410
+ | Show "paid" only after confirming on chain against the approved terms. One transaction proves one job. | `[J16]` |
411
+ | Label provider-supplied text when showing it to an LLM. | spec §10 |
412
+
413
+ Test with the protocol's test vectors (`packages/protocol/test-vectors/`: `challenges.json`, `headers.json`,
414
+ `binding-hash.json`, `job-states.json`, `problems.json`) and against `examples/paid-service`, including the
415
+ recovery drill in §10. Native clients are not listed or badged yet.
416
+
417
+ ## 12. Review for Verified
418
+
419
+ Maintainers check, in order:
420
+
421
+ 1. The adapter card is complete and matches the code.
422
+ 2. The CI conformance run passes against the core version the adapter pins.
423
+ 3. **The signing path:** every call to `signAuthEntry`, `signTransaction`, `authorizeEntry` and `fetch`
424
+ ([SG2]–[SG5], [PM3]–[PM6]).
425
+ 4. **The confirmation path** ([PM7]–[PM9]).
426
+ 5. **The testnet transactions:** one transfer of the right asset, payer, payee and amount; the recovery drill paid
427
+ once.
428
+ 6. No secrets or payment data in logs or telemetry ([AD5]). Pinned dependencies, no install scripts.
429
+ 7. The row in [README.md](README.md#listed-adapters) records the reviewed version. A new major version is reviewed
430
+ again.
431
+
432
+ ## 13. Pull request
433
+
434
+ A PR to this repository adds one row to the [Listed adapters](README.md#listed-adapters) table:
435
+
436
+ ```markdown
437
+ | <Name> (`<factory or method id>`) | Signer \| Payment method | Verified | [<package>](<source URL>) <reviewed version> |
438
+ ```
439
+
440
+ PR description:
441
+
442
+ ```markdown
443
+ ## Integration: <name>
444
+
445
+ Adapter card: <link to the README section>
446
+ Issue: #<number>
447
+
448
+ - [ ] Adapter card in the package README (§9)
449
+ - [ ] Conformance checks pass in CI: <link to the run>
450
+ - [ ] Testnet paid job: <transaction hash>, `confirmed: true`
451
+ - [ ] Testnet recovery drill: one job, one transfer: <transaction hash>
452
+ - [ ] Never submits, sends or settles a payment itself ([SG3], [PM4])
453
+ - [ ] Signs only the approved transfer ([PM3]); self-test output attached (payment methods)
454
+ - [ ] No keys, signed payloads or job tokens in logs or telemetry ([AD5])
455
+ - [ ] Security contact: <email or URL>
456
+ ```
457
+
458
+ A change to the core itself (a new built-in, or a change to `strategies.ts`) is an ordinary PR. It must keep
459
+ CG1–CG10 and their tests, and update this file and [payment-client.md](../payment-client.md).
460
+
461
+ ## 14. Known gaps on our side
462
+
463
+ | Gap | What it means for you | Plan |
464
+ | --- | --- | --- |
465
+ | `payment-client` is at 0.x, and its interfaces carry no version of their own | A minor release (0.1 to 0.2) may change the strategy interfaces an adapter implements: pin it | Every change to them is called out in its PR and the release notes. 1.0 makes them the stable API. |
466
+ | Conformance doesn't exercise `createPayload()` | [PM3] is checked by review and the §8.5 self-test | A signing check: a recording signer plus preimage decoding |
467
+ | The core doesn't check what is signed | A buggy method could sign other terms | A guarded signer: the core wraps the `Signer` it passes to `createPayload()` and refuses any preimage that isn't the approved transfer |
468
+ | `policy.agentAddress` isn't compared with `signer.address` | A policy can run under another account | A fail-closed check in the core |
469
+ | `matches()` doesn't see the signer | A smart-account method can't pair itself with its signer | Configure methods per signer. Maybe `matches(requirements, signer)`. |
470
+ | `fakeChain` and `transferEvent` aren't exported | Copy them (§8.4) | Export them from `./conformance` |
471
+ | `C…` accounts can't pay through `x402ExactStellar` | Smart accounts need their own method ([PM11]) | The smart-account spike (erc-compatibility.md T5) |
472
+ | `createPayload()` has no timeout | An open wallet popup keeps that job's `pay()` pending | Abort after `maxTimeoutSeconds` |
473
+ | No conformance suite for native clients | Test with the vectors and drills (§11) | Planned |
474
+ | The web app doesn't show trust levels | — | Planned |
475
+
476
+ ## 15. Common mistakes
477
+
478
+ - Reading `opts.networkPassphrase` in `signAuthEntry`. The SDK doesn't pass it.
479
+ - Returning a signed XDR entry instead of the raw base64 signature.
480
+ - Implementing `budget()` for a limit the wallet doesn't enforce itself. It gives users a false guarantee. The
481
+ client's spending policy already covers off-chain limits.
482
+ - `matches()` that accepts every `exact` entry, including `stellar:pubnet`, when only testnet was tested.
483
+ - Confirming from the provider's receipt or `PAYMENT-RESPONSE` header instead of the chain.
484
+ - Submitting the transaction inside `createPayload()` "to help". It breaks settlement and risks a second charge.
485
+ - Renaming a method id in a minor release. Earlier payments can no longer be confirmed.
486
+ - Installing a smart-account method after `x402ExactStellar()`. The built-in matches first and can't sign.
487
+ - Using `Buffer` in a browser adapter.
@@ -0,0 +1,137 @@
1
+ # Payment Client Integrations, in short
2
+
3
+ How a wallet, a payment service or another payment client works with Stellar Agent Services. This page is the short
4
+ version. If you are an AI coding agent, read [README-AI.md](README-AI.md) instead.
5
+
6
+ ## Why it exists
7
+
8
+ Our payment client pays AI services that follow the [Provider Standard](../standard/README.md). The part that keeps
9
+ money safe, the **core**, is the same for everyone. Two parts plug into it:
10
+
11
+ - **Signer:** who holds the key. A browser wallet, a passkey smart account, a policy wallet.
12
+ - **Payment method:** how a payment is made and proven. Today there is one: x402 `exact`, USDC on Stellar.
13
+
14
+ When you plug in, your users can pay every service that follows the standard, and they keep every safety rule. You
15
+ don't build those rules again.
16
+
17
+ ## Pick a path
18
+
19
+ | You have | You build | Examples |
20
+ | --- | --- | --- |
21
+ | A wallet that holds keys | A **signer** adapter, `signer-<name>` | Freighter through Stellar Wallets Kit, an ERC-8196 policy wallet |
22
+ | A new way to pay or to prove a payment | A **payment method** adapter, `method-<name>` | A smart account that signs in its own format. A new x402 scheme, which needs a change to the standard first. |
23
+ | Your own agent, wallet or app | A **native client** that follows the standard's client rules | Your code, with no adapter |
24
+
25
+ ```
26
+ Your app or agent
27
+ │
28
+ ▼
29
+ payment-client core: price check · limits · approval · journal · recovery
30
+ │ │
31
+ ▼ ▼
32
+ Signer: who signs Payment method: how to pay and prove it
33
+ (your wallet) (x402 exact on Stellar, or yours)
34
+ ```
35
+
36
+ ## What the core does for you
37
+
38
+ | Promise | How |
39
+ | --- | --- |
40
+ | The price is checked | The payment request must match the price, payee and network the service published. |
41
+ | Limits are checked | Per-payment and daily limits, plus your wallet's own budget, before anything is signed. |
42
+ | The user approves | Every payment, or only those above an amount the user chose. |
43
+ | One signature per job | Even when a wallet popup is slow or the user clicks twice. |
44
+ | Saved before sending | A journal entry is written before the payment leaves, so a crash never leads to paying twice. |
45
+ | Recovery never pays | A lost answer is fetched again. Nothing is signed again. |
46
+ | "Paid" means proven | The transaction is checked on chain against what the user approved, not against what the service says. |
47
+
48
+ ## What only you can get right
49
+
50
+ The core can't see inside your adapter. In particular, **it does not read what your code signs.** So:
51
+
52
+ **A signer must:**
53
+ - sign only as its own address, and keep that address for as long as it exists
54
+ - sign only what it is asked to sign, and never send a transaction itself
55
+ - report a budget only if the wallet enforces it on chain, in base units, without asking the user anything
56
+ - throw an error when the user cancels or it can't sign
57
+
58
+ **A payment method must:**
59
+ - have a fixed id that it never renames
60
+ - accept only payments it can make
61
+ - sign exactly the approved payment: this asset, this amount, this payee, this network, and nothing else
62
+ - never send or settle the payment itself
63
+ - confirm a payment only when the chain shows what the user approved, never because the service said so
64
+
65
+ ## What we need from you
66
+
67
+ 1. **Your own package** (`signer-<name>` or `method-<name>`). It uses our types. We never depend on it.
68
+ 2. **The conformance checks passing** in your CI. They ship with the payment client.
69
+ 3. **Two testnet runs:** one paid job with your adapter, and one interrupted job recovered with exactly one payment.
70
+ 4. **An adapter card** in your README: what it signs, where keys live, which addresses and networks, whether it has a
71
+ budget, and a security contact. The template is in [README-AI.md §9](README-AI.md#9-adapter-card).
72
+ 5. **Open source code** that we can review.
73
+ 6. **No custody:** an adapter whose server holds users' keys can't be verified.
74
+
75
+ ## What you get from us
76
+
77
+ - The `Signer` and `PaymentMethod` interfaces and the conformance checks
78
+ - Every promise above, plus the journal and recovery, with no extra work
79
+ - An approval screen, spending limits and payment history, in our web app and CLI
80
+ - A code review and a "verified" badge that users see
81
+ - A listing on this page, with a link
82
+ - An example service and failure drills to test against
83
+
84
+ ## Trust levels
85
+
86
+ | Level | Meaning | Users see |
87
+ | --- | --- | --- |
88
+ | Built-in | Ours, inside the payment client | No label |
89
+ | Verified | Passes the checks and the testnet runs, and we reviewed the code | A "verified" badge |
90
+ | Community | Anyone's adapter that we have not reviewed | A warning at setup |
91
+
92
+ ## How to submit
93
+
94
+ 1. Open an issue called **"Integration: \<name\>"** with your adapter card. We confirm the slot, and whether the
95
+ standard needs a change first.
96
+ 2. Build and test the adapter in your own repository.
97
+ 3. Open a pull request here that adds a row to [Listed adapters](#listed-adapters), with the checklist below.
98
+ 4. We review the signing and confirmation code, run the checks and look at the testnet transactions.
99
+ 5. You are listed as Verified. Each new major version is reviewed again.
100
+
101
+ **Pull request checklist** (copy it into the PR description):
102
+
103
+ ```markdown
104
+ - [ ] Adapter card in the package README
105
+ - [ ] Conformance checks pass in CI: <link to the run>
106
+ - [ ] Testnet paid job: <transaction hash>, confirmed by the client
107
+ - [ ] Testnet recovery drill: one job, one transfer: <transaction hash>
108
+ - [ ] The adapter never submits, sends or settles a payment itself
109
+ - [ ] No keys, signed payloads or job tokens in logs or telemetry
110
+ - [ ] Security contact: <email or URL>
111
+ ```
112
+
113
+ ## Listed adapters
114
+
115
+ | Adapter | Slot | Level | Where |
116
+ | --- | --- | --- | --- |
117
+ | Local key (`localKeySigner`) | Signer | Built-in | [payment-client](../../packages/payment-client/src/local-key-signer.ts) |
118
+ | x402 exact on Stellar (`x402-exact-stellar`) | Payment method | Built-in | [payment-client](../../packages/payment-client/src/x402-exact-stellar.ts) |
119
+
120
+ Planned, not built: Stellar Wallets Kit, passkey smart accounts, Eunomia, ERC-8196 policy wallets. See
121
+ [payment-client.md](../payment-client.md#strategies-signers-and-payment-methods).
122
+
123
+ ## Not ready on our side yet
124
+
125
+ - The payment client is at 0.x: a minor release may change the interfaces an adapter implements. Pin its version.
126
+ - The conformance checks don't test what a payment method signs. We check it by hand in review.
127
+ - Smart accounts (`C…`) can't pay through the built-in payment method yet. They need their own method.
128
+ - There is no conformance suite for native clients yet. Test against the example service and its drills.
129
+ - The web app doesn't show trust levels yet.
130
+
131
+ ## Learn more
132
+
133
+ - [AI-readable version](README-AI.md)
134
+ - [Payment client design](../payment-client.md)
135
+ - [Provider Standard, in short](../standard/README.md), and its [client rules](../standard/README-AI.md#14-client-rules-agents-wallets-apps)
136
+ - [Payment client package](../../packages/payment-client/README.md)
137
+ - [Example service](../../examples/paid-service/README.md)