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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/docs.d.ts +35 -0
  4. package/dist/docs.d.ts.map +1 -0
  5. package/dist/docs.js +92 -0
  6. package/dist/docs.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +6 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/server.d.ts +14 -0
  12. package/dist/server.d.ts.map +1 -0
  13. package/dist/server.js +125 -0
  14. package/dist/server.js.map +1 -0
  15. package/package.json +40 -4
  16. package/repository/AGENTS.md +162 -0
  17. package/repository/CONTRIBUTING.md +95 -0
  18. package/repository/IMPLEMENTATION_PLAN.md +560 -0
  19. package/repository/README.md +80 -0
  20. package/repository/SECURITY.md +22 -0
  21. package/repository/apps/wallet/README.md +78 -0
  22. package/repository/docs/client-integration/README-AI.md +495 -0
  23. package/repository/docs/client-integration/README.md +138 -0
  24. package/repository/docs/dx-improvements.md +670 -0
  25. package/repository/docs/erc-compatibility.md +163 -0
  26. package/repository/docs/job-recovery.md +201 -0
  27. package/repository/docs/payment-client.md +290 -0
  28. package/repository/docs/provider-setup-guide.md +348 -0
  29. package/repository/docs/provider-standard.md +728 -0
  30. package/repository/docs/recovery/README-AI.md +288 -0
  31. package/repository/docs/recovery/README.md +119 -0
  32. package/repository/docs/sow-closeout.md +67 -0
  33. package/repository/docs/standard/README-AI.md +614 -0
  34. package/repository/docs/standard/README.md +108 -0
  35. package/repository/docs/verification.md +504 -0
  36. package/repository/examples/paid-service/README.md +33 -0
  37. package/repository/examples/python-upstream/README.md +44 -0
  38. package/repository/llms.txt +65 -0
  39. package/repository/packages/create-stellar-service/README.md +143 -0
  40. package/repository/packages/dev-mcp/README.md +51 -0
  41. package/repository/packages/payment-client/README.md +98 -0
  42. package/repository/packages/protocol/README.md +15 -0
  43. package/repository/packages/protocol/schemas/manifest.schema.json +272 -0
  44. package/repository/packages/protocol/test-vectors/README.md +45 -0
  45. package/repository/packages/provider-kit/README.md +509 -0
  46. package/repository/packages/provider-kit/schemas/gateway-config.schema.json +140 -0
  47. package/repository/packages/verifier/README.md +131 -0
  48. package/repository/plugins/stellar-agent-services/README.md +31 -0
@@ -0,0 +1,22 @@
1
+ # Security
2
+
3
+ These boundaries apply to every package in this repository.
4
+
5
+ 1. **Discovery holds no keys.** `stellar-agent-search` stays read-only and keyless. It may import only the
6
+ dependency-free `protocol` package.
7
+ 2. **Provider runtimes hold no keys.** A provider server knows only its `payTo` public address. Owner
8
+ operations are signed in the owner's own wallet, on an approval page the CLI serves on 127.0.0.1 behind a
9
+ one-time token, or with `--source` through the Stellar CLI keychain; never through environment variables. The
10
+ CLI checks that a signed transaction is the one it built before sending it. In gateway mode, the only
11
+ credential is the upstream API's own key, if it has one, and never a Stellar secret key. The only keys the CLI
12
+ makes are throwaway testnet ones, held in memory and never written: `provider-kit try`'s payer, and the payout
13
+ account `provider-kit dev` makes while `PAY_TO` is empty. That key signs the account's USDC trustline and is
14
+ dropped; the provider gets only the public address.
15
+ 3. **Only the payment client signs payments**, once per approved job, after the user approves the exact
16
+ price, recipient, asset and network.
17
+ 4. **Never pay twice.** The job identity is fixed before payment. Recovery never re-sends a payment
18
+ credential, and an uncertain settlement is never re-charged.
19
+ 5. **Fail closed.** Unknown or unverifiable states stop the flow.
20
+ 6. **One network per flow.** Testnet and mainnet are never mixed in one run.
21
+
22
+ To report a vulnerability, contact the maintainers privately rather than opening a public issue.
@@ -0,0 +1,78 @@
1
+ # Agent Wallet (web approval page)
2
+
3
+ The page where a person sees the exact payment and approves it before anything is paid: the "web app" surface
4
+ of the payment client ([docs/payment-client.md](../../docs/payment-client.md)). It is a static page with no
5
+ backend. Every safety step runs in [`packages/payment-client`](../../packages/payment-client), in the browser.
6
+
7
+ **Status: minimal page (Week 1), Stellar testnet only.** It creates a test wallet, loads any Provider Standard v1
8
+ provider, builds the request form from the service's input schema, shows the approval screen, pays once, confirms
9
+ the transfer on chain and recovers lost jobs.
10
+
11
+ ## Run it
12
+
13
+ ```bash
14
+ npm install && npm run build # builds the packages the page uses
15
+
16
+ # The sample provider (any public G... address with a USDC trustline receives the payments)
17
+ PAY_TO=G... node examples/paid-service/dist/server.js
18
+
19
+ # The page, on http://localhost:5173
20
+ npm run dev --workspace @stellar-agent-services/wallet
21
+ ```
22
+
23
+ Then:
24
+
25
+ 1. **Create test wallet.** This makes a key in the browser, gets test XLM from Friendbot and buys 5 test USDC on
26
+ the testnet exchange.
27
+ 2. **Use localhost:8787.** This loads the sample provider's manifest.
28
+ 3. **Pick a service and review the payment.** The approval screen shows the price, the full payee address, when you
29
+ are charged, and the challenge and spending-limit checks.
30
+ 4. **Approve.** The page follows the payment and shows "Paid" only once the `transfer` on chain matches what you
31
+ approved.
32
+
33
+ To show spending control, set the limit per payment to 0.01 and review `delayed-report` (0.02): the payment is
34
+ blocked before anything is signed.
35
+
36
+ For the Job Recovery demo, start the provider with `DRILL_LOSE_RESPONSE=1 REPORT_DELAY_SECONDS=10` and pay
37
+ `delayed-report`. The page says it can't tell whether the payment went through. **Recover job** then collects
38
+ the result with no second payment.
39
+
40
+ `npm run build --workspace @stellar-agent-services/wallet` writes the static page to `apps/wallet/dist`.
41
+
42
+ ## How it keeps payments safe
43
+
44
+ - **Core.** The page never decides whether something is paid. It calls `quote()`, `pay()`, `decline()` and
45
+ `recover()` on the core, and the core runs every check (docs/payment-client.md, "One payment, step by step").
46
+ - **Decline by default.** Closing the approval screen in any way declines: the Decline button, the close button,
47
+ or Escape. The Decline button has the focus, so a stray Enter never pays.
48
+ - **Journal.** The journal and job tokens live in IndexedDB (`@stellar-agent-services/payment-client/browser`).
49
+ - Writes use strict durability, so `sending` is on disk before the paid request goes out.
50
+ - The page runs in one tab at a time (a Web Lock), so the journal has one writer.
51
+ - If the journal can't be opened, nothing can be paid.
52
+ - **Key.** The test wallet's key is in this browser's site storage. That is fine for test money, never for real
53
+ money: real wallets will plug in as signers (Stellar Wallets Kit, passkey accounts).
54
+ - **No outside code.** The page loads no third-party code, and a Content Security Policy blocks inline and outside
55
+ scripts. `'unsafe-eval'` is allowed for Ajv, which compiles the manifest schema.
56
+ - **Local providers.** `http://localhost` providers are accepted only while the page itself runs on localhost, as
57
+ with `stellar-pay --dev`.
58
+
59
+ ## Files
60
+
61
+ | File | Contents |
62
+ | --- | --- |
63
+ | `src/lib/agent-wallet.svelte.ts` | The page's state and actions on top of the core; follows the journal for progress |
64
+ | `src/lib/schema-form.ts` | A service's `inputSchema` → form fields; anything else gets a JSON editor |
65
+ | `src/lib/jobs.ts` | Each job's state from the journal, for Recent payments |
66
+ | `src/lib/stellar.ts` | Test wallet balances and funding (Horizon, Friendbot, testnet exchange) |
67
+ | `src/lib/storage.ts` | The test wallet, the spending policy and service titles in site storage |
68
+ | `src/lib/format.ts` | Amounts, addresses and times for people |
69
+ | `src/components/` | `PayFlow` (provider, service, request), `ApprovalDialog`, `PaymentCard`, `WalletCard`, `LimitsCard`, `RecentPayments` |
70
+
71
+ ## Not done yet
72
+
73
+ - Signers other than the test key: Stellar Wallets Kit (Freighter and others) and passkey accounts.
74
+ - Approval links from the MCP server, so an agent above its budget can send its human here.
75
+ - Verification badges on the approval screen (they need the verifier).
76
+ - Paying without a prompt below an amount (`autoApproveUpTo`). The page always asks.
77
+ - Precompiling the manifest validator, so the CSP can drop `'unsafe-eval'`.
78
+ - Hosting: any static host works, and it should send `frame-ancestors 'none'` as a header.
@@ -0,0 +1,495 @@
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/verification.ts` | Application-trusted evidence source and subject/freshness checks [V1] |
58
+ | `packages/payment-client/src/local-key-signer.ts` | Built-in signer: `createEd25519Signer` from `@x402/stellar` |
59
+ | `packages/payment-client/src/x402-exact-stellar.ts` | Built-in method, id `x402-exact-stellar` |
60
+ | `packages/payment-client/src/onchain.ts` | `rpcConfirmPayment`, `checkTransfer`, `TransactionReader`. Reuse them in your `confirm()`. |
61
+ | `packages/payment-client/src/conformance.ts` | `signerConformance`, `paymentMethodConformance` (entry point `./conformance`) |
62
+ | `packages/payment-client/test/conformance.test.ts` | How the built-ins run conformance, and the broken adapters it must fail |
63
+ | `packages/payment-client/test/support/chain.ts` | `fakeChain`, `transferEvent`. Not exported: copy them (§8.4). |
64
+ | `packages/payment-client/test/flow.test.ts`, `describe("strategies")` | The core's behavior around strategies |
65
+ | `packages/provider-kit/src/payments/payload-checks.ts` | What a provider checks in your payload before its facilitator sees it |
66
+ | `examples/paid-service` | The provider to pay in the testnet runs (§10) |
67
+ | `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 |
68
+
69
+ ## 3. Interfaces
70
+
71
+ From `strategies.ts`, with the SEP-43 types expanded (`@stellar/stellar-sdk/contract`):
72
+
73
+ ```ts
74
+ /** Who holds the key: ClientStellarSigner from @x402/stellar, plus budget(). */
75
+ interface Signer {
76
+ address: string; // G… or C…
77
+ signAuthEntry: SignAuthEntry;
78
+ signTransaction?: SignTransaction; // never called by the core or by x402 exact
79
+ /** A wallet's own on-chain limit for `asset`, in base units. Undefined: no limit. */
80
+ budget?(network: StellarNetwork, asset: string): Promise<string | undefined>;
81
+ }
82
+
83
+ type SignAuthEntry = (
84
+ authEntry: string, // base64 xdr.HashIdPreimage (envelopeTypeSorobanAuthorization)
85
+ opts?: { networkPassphrase?: string; address?: string },
86
+ ) => Promise<{ signedAuthEntry: string; signerAddress?: string } & { error?: { message: string; code: number } }>;
87
+
88
+ /** How an x402 challenge entry is paid, and how that payment is proven. */
89
+ interface PaymentMethod {
90
+ readonly id: string; // journaled with each payment
91
+ matches(requirements: PaymentRequirements): boolean;
92
+ createPayload(requirements: PaymentRequirements, signer: Signer): Promise<SignedPayload>;
93
+ confirm(transaction: string, expected: ExpectedPayment): Promise<Confirmation>;
94
+ }
95
+
96
+ type SignedPayload = Pick<PaymentPayload, "x402Version" | "payload">; // @x402/core/types
97
+ interface ExpectedPayment { network: StellarNetwork; asset: string; amount: string; payTo: string; payer: string }
98
+ interface Confirmation { confirmed: boolean; reason?: string }
99
+ ```
100
+
101
+ Facts the types don't tell you:
102
+
103
+ - The SDK calls `signAuthEntry(preimage, { address })` **without** `networkPassphrase`. Take the network when the
104
+ signer is built. The preimage already commits to the network id.
105
+ - `signedAuthEntry` is the base64 raw 64-byte ed25519 signature over `sha256(preimage bytes)`, not a signed XDR
106
+ entry.
107
+ - A returned `error` makes the SDK throw (`handleWalletError`). Either way the core journals `refused`.
108
+ - `budget()` and `ExpectedPayment.amount` are base units of the asset (USDC: 7 decimals), matching `^\d+$`.
109
+ - `requirements` is the challenge's single `accepts` entry: `scheme`, `network`, `asset`, `amount`, `payTo`,
110
+ `maxTimeoutSeconds`, `extra.areFeesSponsored`.
111
+ - The core sends `{ ...signed, x402Version: 2, resource, accepted }`. It sets `resource` and `accepted` itself.
112
+ Any other key you return passes through to the provider unchecked.
113
+ - `confirm(transaction, …)` gets the hash from the provider's job view (`job.payment.transaction`). It is
114
+ untrusted input.
115
+ - `PaymentClientOptions.methods`: the first method whose `matches()` is true wins. The default is
116
+ `[x402ExactStellar({ rpcUrl })]`. An empty list or duplicate ids throw a `TypeError` in the constructor.
117
+
118
+ ## 4. What the core enforces (CG)
119
+
120
+ These hold for every adapter. Don't re-implement them in an adapter.
121
+
122
+ | Id | Guarantee | Code (`client.ts`) | Test (`flow.test.ts`) |
123
+ | --- | --- | --- | --- |
124
+ | 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" |
125
+ | 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" |
126
+ | 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" |
127
+ | 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 …" |
128
+ | 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" |
129
+ | 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" |
130
+ | 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" |
131
+ | CG8 | `sending` with `{ network, asset, amount, payTo, payer: signer.address, method: id }` is durable before the paid request leaves. | `payOnce()` | same as CG3 |
132
+ | 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" |
133
+ | CG10 | `recover()` never calls `createPayload()`. | `recover()` | "recovers a lost response without paying again" |
134
+
135
+ CG4 includes configured verification restrictions [V1]. The application installs `verificationSource`, not the
136
+ signer or payment method. Evidence must match the approved manifest hash, URL, payee, network and agent id and
137
+ remain within its validity window. No source, an outage or a missing required metric blocks signing. See
138
+ `test/verification.test.ts` and [the design](../payment-client.md#verification-source). `minVerificationScore`
139
+ retains ERC-8196's maximum-risk semantics; it is not the higher-is-better receipt score. Recovery does not read
140
+ verification evidence.
141
+
142
+ Journal on the happy path: `intent, approved, sending, settled, result`. When the wallet doesn't sign:
143
+ `intent, approved, refused`.
144
+
145
+ **What the core does not enforce.** This is why the `SG` and `PM` rules exist.
146
+
147
+ - **It doesn't inspect what is signed.** `createPayload()` receives the whole `Signer`, and the core never decodes
148
+ its result. A method that authorizes another amount or payee is refused by an honest provider
149
+ (`payload-checks.ts`) and by its facilitator. But by then a signed authorization has left the process, and anyone
150
+ holding it can submit it until its expiration ledger.
151
+ - **It doesn't bind the policy to the signer.** `policy.agentAddress` is never compared with `signer.address`.
152
+ - **It reads `signer.address` again after signing**, to journal `payer`. If the address changed during signing,
153
+ confirmation fails. The user sees "not confirmed"; nothing is charged twice.
154
+ - **It doesn't time out `createPayload()`.** A wallet popup left open keeps that job's `pay()` pending. A signature
155
+ produced after `maxTimeoutSeconds` is refused by the provider (`402`, journaled `failed`, `charged: false`).
156
+
157
+ ## 5. Signer rules (SG)
158
+
159
+ | Id | Rule | Checked by |
160
+ | --- | --- | --- |
161
+ | 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. |
162
+ | 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" |
163
+ | 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 |
164
+ | 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 |
165
+ | 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. |
166
+ | 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. |
167
+ | SG7 | `budget()` SHOULD answer within about 2 s. It runs on every `quote()` and `pay()`. | Review |
168
+ | SG8 | Omit `signTransaction` unless your wallet needs it elsewhere. The core never calls it. | — |
169
+ | SG9 | The adapter card declares where the key lives. A signer whose key a server holds for the user can't be Verified. | Review |
170
+ | SG10 | A signer that prompts a human SHOULD show the asset, amount, payee and network decoded from the preimage. | Review |
171
+
172
+ ## 6. Payment method rules (PM)
173
+
174
+ | Id | Rule | Checked by |
175
+ | --- | --- | --- |
176
+ | 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. |
177
+ | 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" |
178
+ | 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. |
179
+ | 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 |
180
+ | 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 |
181
+ | 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 |
182
+ | 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" |
183
+ | 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. |
184
+ | PM9 | `confirm()` keeps working for payments already journaled under its id. Changing what it accepts is a major version. | Review |
185
+ | 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 |
186
+ | 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 |
187
+
188
+ ## 7. Package rules (AD)
189
+
190
+ | Id | Rule |
191
+ | --- | --- |
192
+ | AD1 | Its own package, named `signer-<name>` or `method-<name>` (under your own npm scope). |
193
+ | 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. |
194
+ | 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. |
195
+ | AD4 | CI runs the conformance checks (§8.4) on every change, and they all pass. |
196
+ | AD5 | No keys, preimages, signatures, payloads or job tokens in logs, error messages, analytics or telemetry. Errors name the cause, not the data. |
197
+ | AD6 | Semver. Changing the method `id`, or what the address, `budget()` or `confirm()` means, is a major version. |
198
+ | AD7 | Public source under an open-source license (MIT or Apache-2.0 preferred), and a security contact. |
199
+ | AD8 | The package README contains the adapter card (§9). |
200
+
201
+ ## 8. Templates
202
+
203
+ ### 8.1 Signer for a SEP-43 wallet
204
+
205
+ ```ts
206
+ import type { StellarNetwork } from "@stellar-agent-services/protocol";
207
+ import type { Signer } from "@stellar-agent-services/payment-client";
208
+ import { getNetworkPassphrase } from "@x402/stellar";
209
+
210
+ /** The SEP-43 subset of your wallet's API. Replace it with the real one. */
211
+ export interface WalletApi {
212
+ getAddress(): Promise<{ address: string }>;
213
+ signAuthEntry(
214
+ authEntry: string,
215
+ opts: { networkPassphrase: string; address: string },
216
+ ): Promise<{ signedAuthEntry: string; signerAddress?: string; error?: { message: string; code: number } }>;
217
+ }
218
+
219
+ export async function walletSigner(wallet: WalletApi, network: StellarNetwork): Promise<Signer> {
220
+ const { address } = await wallet.getAddress(); // read once [SG1]
221
+ const networkPassphrase = getNetworkPassphrase(network); // from construction, not opts [SG4]
222
+ return {
223
+ address,
224
+ async signAuthEntry(authEntry) {
225
+ const result = await wallet.signAuthEntry(authEntry, { networkPassphrase, address });
226
+ if (result.error !== undefined) throw new Error(`the wallet did not sign: ${result.error.message}`); // [SG5]
227
+ if (result.signedAuthEntry === "") throw new Error("the wallet returned no signature");
228
+ if (result.signerAddress !== undefined && result.signerAddress !== address) {
229
+ throw new Error("the wallet signed with another account"); // [SG2]
230
+ }
231
+ return { signedAuthEntry: result.signedAuthEntry, signerAddress: address };
232
+ },
233
+ };
234
+ }
235
+ ```
236
+
237
+ ### 8.2 A wallet with an on-chain budget
238
+
239
+ ```ts
240
+ // Inside the object walletSigner returns. `policyWallet.remaining` is your wallet's own read-only call.
241
+ async budget(asked: StellarNetwork, asset: string) {
242
+ if (asked !== network) throw new Error(`this wallet is on ${network}, not ${asked}`);
243
+ const remaining: bigint | null = await policyWallet.remaining(address, asset); // no prompt, no signature [SG6]
244
+ return remaining === null ? undefined : remaining.toString(); // base units
245
+ },
246
+ ```
247
+
248
+ ### 8.3 Payment method
249
+
250
+ ```ts
251
+ import { rpcConfirmPayment, type PaymentMethod, type TransactionReader } from "@stellar-agent-services/payment-client";
252
+
253
+ export const METHOD_ID = "x402-exact-stellar-example"; // [PM1]
254
+
255
+ export function exampleMethod(options: { rpcUrl?: string; server?: TransactionReader } = {}): PaymentMethod {
256
+ return {
257
+ id: METHOD_ID,
258
+ // Pure and total, including for {} [PM2]. Add your own asset and account conditions.
259
+ matches: (r) => r?.scheme === "exact" && r.network === "stellar:testnet" && typeof r.payTo === "string" && typeof r.amount === "string",
260
+ async createPayload(requirements, signer) {
261
+ // Build ONE asset.transfer(signer.address, payTo, amount) invocation and simulate it. Authorize only the
262
+ // payer's auth entry, expiring within maxTimeoutSeconds. Return { x402Version: 2, payload: { transaction } }.
263
+ // Never submit it [PM3]–[PM6].
264
+ throw new Error("not implemented");
265
+ },
266
+ confirm: rpcConfirmPayment(options), // the on-chain transfer check [PM7]; `server` lets tests pass a fake chain
267
+ };
268
+ }
269
+ ```
270
+
271
+ Accept a `server?: TransactionReader` option, as `x402ExactStellar` does, so conformance runs offline.
272
+
273
+ ### 8.4 Conformance tests
274
+
275
+ ```ts
276
+ import { USDC_ASSET } from "@stellar-agent-services/protocol";
277
+ import type { ExpectedPayment } from "@stellar-agent-services/payment-client";
278
+ import { paymentMethodConformance, signerConformance } from "@stellar-agent-services/payment-client/conformance";
279
+ import { Keypair } from "@stellar/stellar-sdk";
280
+ import type { PaymentRequirements } from "@x402/core/types";
281
+ import { describe, it } from "vitest";
282
+ import { fakeChain, transferEvent } from "./support/chain.js"; // copied from payment-client/test/support/chain.ts
283
+
284
+ const network = "stellar:testnet";
285
+ // A test double of your wallet that signs with a real key, or the real wallet in a browser test runner.
286
+ const signer = await walletSigner(testWallet(), network);
287
+
288
+ describe("signer conformance", () => {
289
+ for (const check of signerConformance(signer, { network })) it(check.name, check.run);
290
+ });
291
+
292
+ describe("payment method conformance", () => {
293
+ const payer = Keypair.random().publicKey();
294
+ const payTo = Keypair.random().publicKey();
295
+ const expected: ExpectedPayment = { network, asset: USDC_ASSET[network], amount: "100000", payTo, payer };
296
+ const transaction = "ab".repeat(32);
297
+ const server = fakeChain({ [transaction]: [transferEvent({ asset: expected.asset, from: payer, to: payTo })] });
298
+ const requirements = {
299
+ scheme: "exact", network, asset: expected.asset, amount: expected.amount, payTo,
300
+ maxTimeoutSeconds: 60, extra: { areFeesSponsored: true },
301
+ } as PaymentRequirements;
302
+ for (const check of paymentMethodConformance(exampleMethod({ server }), { requirements, settled: { transaction, expected } })) {
303
+ it(check.name, check.run);
304
+ }
305
+ });
306
+ ```
307
+
308
+ `transferEvent` writes an amount of `100000` by default, so keep `expected.amount` at `"100000"` or pass `data`.
309
+
310
+ ### 8.5 Self-test for what a method signs (PM3)
311
+
312
+ Conformance doesn't call `createPayload()` yet. Until it does, wrap the signer, record every preimage, run
313
+ `createPayload()` against testnet with a funded payer (simulation needs the USDC balance), and check each preimage:
314
+
315
+ ```ts
316
+ import { Address, hash, scValToNative, xdr } from "@stellar/stellar-sdk";
317
+ import { getNetworkPassphrase } from "@x402/stellar";
318
+
319
+ function assertApprovedTransfer(preimage: string, r: PaymentRequirements, payer: string): void {
320
+ const auth = xdr.HashIdPreimage.fromXDR(preimage, "base64").sorobanAuthorization();
321
+ if (!auth.networkId().equals(hash(Buffer.from(getNetworkPassphrase(r.network))))) throw new Error("wrong network");
322
+ const invocation = auth.invocation();
323
+ if (invocation.subInvocations().length !== 0) throw new Error("sub-invocations");
324
+ const call = invocation.function().contractFn();
325
+ const [from, to, amount] = call.args().map((arg) => scValToNative(arg) as unknown);
326
+ if (Address.fromScAddress(call.contractAddress()).toString() !== r.asset) throw new Error("wrong asset");
327
+ if (call.functionName().toString() !== "transfer") throw new Error("not a transfer");
328
+ if (from !== payer || to !== r.payTo || amount !== BigInt(r.amount)) throw new Error("wrong transfer");
329
+ }
330
+ ```
331
+
332
+ ## 9. Adapter card
333
+
334
+ Put this under `## Adapter card` in the package README. Every field is required; write `none` where it applies.
335
+
336
+ ```yaml
337
+ name: Example Wallet signer
338
+ package: "@example/signer-example" # npm name, or a repo path until published
339
+ version: 0.1.0
340
+ slot: signer # signer | payment-method
341
+ methodId: none # payment methods: the journaled id
342
+ addresses: [G] # G, C
343
+ networks: [stellar:testnet] # stellar:testnet, stellar:pubnet
344
+ budget: none # none | on-chain: <the contract or mechanism that enforces it>
345
+ humanPrompt: per-payment # never | per-payment | above-a-limit
346
+ keyCustody: browser extension on the user's device # where the key lives; "server" can't be Verified
347
+ signs: one SAC transfer(payer, payTo, amount) auth entry per payment
348
+ submits: never
349
+ dependencies: ["@stellar/stellar-sdk@16.4.1", "@x402/stellar@2.28.0"]
350
+ license: MIT
351
+ source: https://github.com/example/signer-example
352
+ security: security@example.com
353
+ audits: none
354
+ conformance: <link to a passing CI run>
355
+ testnet:
356
+ paid: <transaction hash>
357
+ recovered: <transaction hash>
358
+ ```
359
+
360
+ ## 10. Testnet runs (evidence for Verified)
361
+
362
+ Set up a provider wallet, a funded payer and the example service, as in the
363
+ [payment-client README](../../packages/payment-client/README.md#try-it-on-testnet):
364
+
365
+ ```bash
366
+ npm install && npm run build
367
+ STELLAR_PAY_HOME=~/.stellar-pay-provider npx stellar-pay wallet create
368
+ STELLAR_PAY_HOME=~/.stellar-pay-provider npx stellar-pay wallet fund --usdc 0
369
+ PAY_TO=G... node examples/paid-service/dist/server.js
370
+ ```
371
+
372
+ The CLI only uses the built-in signer, so pay with your adapter from a short script:
373
+
374
+ ```ts
375
+ import { defaultPolicy, Journal, memoryJournalStorage, memorySecretStore, PaymentClient } from "@stellar-agent-services/payment-client";
376
+
377
+ const signer = await walletSigner(wallet, "stellar:testnet");
378
+ const client = new PaymentClient({
379
+ signer,
380
+ methods: [exampleMethod()], // omit for the default x402ExactStellar()
381
+ policy: defaultPolicy(signer.address, "stellar:testnet"),
382
+ journal: await Journal.open(memoryJournalStorage()),
383
+ secrets: memorySecretStore(),
384
+ allowLocalhost: true,
385
+ });
386
+ const manifest = "http://localhost:8787/.well-known/stellar-agent-services.json";
387
+ const quote = await client.quote(manifest, "text-stats", { text: "Hello agents." });
388
+ if (!client.payable(quote)) throw new Error(quote.challengeViolations.concat(quote.policyViolations.map((v) => v.message)).join("; "));
389
+ const result = await client.pay(quote, { approved: true });
390
+ console.log(result.confirmed, result.job.payment?.transaction); // must be true, with a hash
391
+ ```
392
+
393
+ 1. **Paid job:** `result.confirmed` is `true`. The explorer shows one USDC `transfer` from the payer to `PAY_TO`
394
+ for the price.
395
+ 2. **Recovery drill:** restart the provider with `DRILL_LOSE_RESPONSE=1 REPORT_DELAY_SECONDS=8` and pay
396
+ `delayed-report` with `{ "topic": "…" }`. `pay()` throws `PaymentUncertainError`; call
397
+ `client.recover(error.jobId)`. It must return `confirmed: true`, and the explorer must show exactly one transfer
398
+ for that job.
399
+
400
+ For a browser wallet, run the same flow in [apps/wallet](../../apps/wallet) locally by swapping `localKeySigner`
401
+ in `agent-wallet.svelte.ts` for your signer. Don't commit that change.
402
+
403
+ ## 11. Native clients
404
+
405
+ A native client implements the client side of the spec itself. It gets none of the CG guarantees for free, so it
406
+ must provide them. The rules are in [standard/README-AI.md §14](../standard/README-AI.md#14-client-rules-agents-wallets-apps):
407
+
408
+ | Must | Rules |
409
+ | --- | --- |
410
+ | Fetch the manifest without redirects, at most 256 KiB, I-JSON. Reject another major `standard`. Never fetch remote schemas. | `[M3]`, `[M8]`, `[S5]` |
411
+ | Generate and persist the job id and a 32-byte token before the first request. Send both. | `[J1]` |
412
+ | Compare the challenge with the approved manifest. Refuse to sign on any difference. | `[P4]`, `[P7]` |
413
+ | Show price, payee, network and `charge` before approval. | `[S3]` |
414
+ | Sign once per job. Persist "sending" before the paid request. Never sign again for that job. | `[P9]`; CG6–CG8 |
415
+ | `429` / `503`: wait `Retry-After` plus jitter and resend the identical request, with the same `PAYMENT-SIGNATURE`. Stop before the payment expires. | `[Q8]`, `[S2]` |
416
+ | `202` / `409`: poll no faster than `Retry-After`. HTTP timeout above 30 s. | `[Q9]`, `[J7]`, `[J9]` |
417
+ | `payment_uncertain`: stop polling and tell the user not to pay again. | `[J13]` |
418
+ | Show "paid" only after confirming on chain against the approved terms. One transaction proves one job. | `[J16]` |
419
+ | Label provider-supplied text when showing it to an LLM. | spec §10 |
420
+
421
+ Test with the protocol's test vectors (`packages/protocol/test-vectors/`: `challenges.json`, `headers.json`,
422
+ `binding-hash.json`, `job-states.json`, `problems.json`) and against `examples/paid-service`, including the
423
+ recovery drill in §10. Native clients are not listed or badged yet.
424
+
425
+ ## 12. Review for Verified
426
+
427
+ Maintainers check, in order:
428
+
429
+ 1. The adapter card is complete and matches the code.
430
+ 2. The CI conformance run passes against the core version the adapter pins.
431
+ 3. **The signing path:** every call to `signAuthEntry`, `signTransaction`, `authorizeEntry` and `fetch`
432
+ ([SG2]–[SG5], [PM3]–[PM6]).
433
+ 4. **The confirmation path** ([PM7]–[PM9]).
434
+ 5. **The testnet transactions:** one transfer of the right asset, payer, payee and amount; the recovery drill paid
435
+ once.
436
+ 6. No secrets or payment data in logs or telemetry ([AD5]). Pinned dependencies, no install scripts.
437
+ 7. The row in [README.md](README.md#listed-adapters) records the reviewed version. A new major version is reviewed
438
+ again.
439
+
440
+ ## 13. Pull request
441
+
442
+ A PR to this repository adds one row to the [Listed adapters](README.md#listed-adapters) table:
443
+
444
+ ```markdown
445
+ | <Name> (`<factory or method id>`) | Signer \| Payment method | Verified | [<package>](<source URL>) <reviewed version> |
446
+ ```
447
+
448
+ PR description:
449
+
450
+ ```markdown
451
+ ## Integration: <name>
452
+
453
+ Adapter card: <link to the README section>
454
+ Issue: #<number>
455
+
456
+ - [ ] Adapter card in the package README (§9)
457
+ - [ ] Conformance checks pass in CI: <link to the run>
458
+ - [ ] Testnet paid job: <transaction hash>, `confirmed: true`
459
+ - [ ] Testnet recovery drill: one job, one transfer: <transaction hash>
460
+ - [ ] Never submits, sends or settles a payment itself ([SG3], [PM4])
461
+ - [ ] Signs only the approved transfer ([PM3]); self-test output attached (payment methods)
462
+ - [ ] No keys, signed payloads or job tokens in logs or telemetry ([AD5])
463
+ - [ ] Security contact: <email or URL>
464
+ ```
465
+
466
+ A change to the core itself (a new built-in, or a change to `strategies.ts`) is an ordinary PR. It must keep
467
+ CG1–CG10 and their tests, and update this file and [payment-client.md](../payment-client.md).
468
+
469
+ ## 14. Known gaps on our side
470
+
471
+ | Gap | What it means for you | Plan |
472
+ | --- | --- | --- |
473
+ | `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. |
474
+ | 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 |
475
+ | 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 |
476
+ | `policy.agentAddress` isn't compared with `signer.address` | A policy can run under another account | A fail-closed check in the core |
477
+ | `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)`. |
478
+ | `fakeChain` and `transferEvent` aren't exported | Copy them (§8.4) | Export them from `./conformance` |
479
+ | `C…` accounts can't pay through `x402ExactStellar` | Smart accounts need their own method ([PM11]) | The smart-account spike (erc-compatibility.md T5) |
480
+ | `createPayload()` has no timeout | An open wallet popup keeps that job's `pay()` pending | Abort after `maxTimeoutSeconds` |
481
+ | No conformance suite for native clients | Test with the vectors and drills (§11) | Planned |
482
+ | The web app doesn't show trust levels | — | Planned |
483
+
484
+ ## 15. Common mistakes
485
+
486
+ - Reading `opts.networkPassphrase` in `signAuthEntry`. The SDK doesn't pass it.
487
+ - Returning a signed XDR entry instead of the raw base64 signature.
488
+ - Implementing `budget()` for a limit the wallet doesn't enforce itself. It gives users a false guarantee. The
489
+ client's spending policy already covers off-chain limits.
490
+ - `matches()` that accepts every `exact` entry, including `stellar:pubnet`, when only testnet was tested.
491
+ - Confirming from the provider's receipt or `PAYMENT-RESPONSE` header instead of the chain.
492
+ - Submitting the transaction inside `createPayload()` "to help". It breaks settlement and risks a second charge.
493
+ - Renaming a method id in a minor release. Earlier payments can no longer be confirmed.
494
+ - Installing a smart-account method after `x402ExactStellar()`. The built-in matches first and can't sign.
495
+ - Using `Buffer` in a browser adapter.