@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.
- package/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/docs.d.ts +35 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +92 -0
- package/dist/docs.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +125 -0
- package/dist/server.js.map +1 -0
- package/package.json +40 -4
- package/repository/AGENTS.md +162 -0
- package/repository/CONTRIBUTING.md +95 -0
- package/repository/IMPLEMENTATION_PLAN.md +548 -0
- package/repository/README.md +78 -0
- package/repository/SECURITY.md +22 -0
- package/repository/apps/wallet/README.md +78 -0
- package/repository/docs/client-integration/README-AI.md +487 -0
- package/repository/docs/client-integration/README.md +137 -0
- package/repository/docs/dx-improvements.md +666 -0
- package/repository/docs/erc-compatibility.md +163 -0
- package/repository/docs/job-recovery.md +201 -0
- package/repository/docs/payment-client.md +258 -0
- package/repository/docs/provider-setup-guide.md +348 -0
- package/repository/docs/provider-standard.md +724 -0
- package/repository/docs/recovery/README-AI.md +288 -0
- package/repository/docs/recovery/README.md +119 -0
- package/repository/docs/standard/README-AI.md +611 -0
- package/repository/docs/standard/README.md +108 -0
- package/repository/docs/verification.md +502 -0
- package/repository/examples/paid-service/README.md +24 -0
- package/repository/examples/python-upstream/README.md +44 -0
- package/repository/llms.txt +64 -0
- package/repository/packages/create-stellar-service/README.md +143 -0
- package/repository/packages/dev-mcp/README.md +51 -0
- package/repository/packages/payment-client/README.md +75 -0
- package/repository/packages/protocol/README.md +15 -0
- package/repository/packages/protocol/schemas/manifest.schema.json +272 -0
- package/repository/packages/protocol/test-vectors/README.md +45 -0
- package/repository/packages/provider-kit/README.md +507 -0
- package/repository/packages/provider-kit/schemas/gateway-config.schema.json +140 -0
- package/repository/packages/verifier/README.md +127 -0
- 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)
|