@pooriaarab/foxpay 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 +372 -2
- package/dist/card.d.ts +80 -0
- package/dist/card.js +241 -0
- package/dist/eip712.d.ts +26 -0
- package/dist/eip712.js +96 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/intent.d.ts +134 -0
- package/dist/intent.js +263 -0
- package/dist/testing.d.ts +52 -0
- package/dist/testing.js +99 -0
- package/dist/x402.d.ts +59 -0
- package/dist/x402.js +174 -0
- package/package.json +58 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pooria Arab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,373 @@
|
|
|
1
|
-
#
|
|
1
|
+
# foxpay
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">Agent payments with spend caps and approval for each payment.</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/pooriaarab/foxpay/actions"><img src="https://github.com/pooriaarab/foxpay/actions/workflows/ci.yml/badge.svg" alt="CI"/></a>
|
|
7
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License MIT"/></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
foxpay lets an AI agent ask to pay, and lets a human approve each payment.
|
|
11
|
+
The agent writes an intent: the merchant, the amount, the currency, and the
|
|
12
|
+
reason. foxpay reads the real amount itself, from the merchant page or from
|
|
13
|
+
the `402 Payment Required` answer. Then it asks [foxgate](https://github.com/pooriaarab/foxgate),
|
|
14
|
+
which checks the spend cap and waits for a human. After one approval, foxpay
|
|
15
|
+
pays one time with a card from [foxvault](https://github.com/pooriaarab/foxvault)
|
|
16
|
+
or with an x402 payment on Base Sepolia. Each payment gets a receipt.
|
|
17
|
+
|
|
18
|
+
No real money moves in this repo. The tests and the demo use test cards, a
|
|
19
|
+
fake card provider, the Base Sepolia testnet, and a local fake facilitator.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i @pooriaarab/foxpay
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The npm package is `@pooriaarab/foxpay`: npm refuses the plain name `foxpay` as too similar to an existing package (fox-pay).
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
This example pays a fake x402 API in Node. It runs as written.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { createFoxgate, memoryStore } from "foxgate";
|
|
35
|
+
import { createVault } from "foxvault";
|
|
36
|
+
import { PAY_TOOL, createFoxpay, payTools, x402 } from "@pooriaarab/foxpay";
|
|
37
|
+
import { fakeX402Api } from "@pooriaarab/foxpay/testing";
|
|
38
|
+
|
|
39
|
+
// A Base Sepolia test key and recipient. The fake API stands in for a paid API.
|
|
40
|
+
const WALLET_KEY = "0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318";
|
|
41
|
+
const PAY_TO = "0x209693Bc6afc0C5328bA36FaF03C514EF312287C";
|
|
42
|
+
|
|
43
|
+
const store = memoryStore();
|
|
44
|
+
const { gate, host } = createFoxgate({ tools: payTools(), store });
|
|
45
|
+
// A cap of 0.05 test USDC. foxgate counts test USDC as XTS, in atomic units.
|
|
46
|
+
await host.addGrant({ scope: "pay", domains: ["api.example"], tools: [PAY_TOOL], spendCap: { value: 50_000, currency: "XTS" } });
|
|
47
|
+
|
|
48
|
+
const vault = createVault();
|
|
49
|
+
await vault.initialize();
|
|
50
|
+
await vault.set("vault:wallet", WALLET_KEY, { domains: ["api.example"] });
|
|
51
|
+
|
|
52
|
+
const api = fakeX402Api({ host: "api.example", payTo: PAY_TO, price: 10_000, body: "sunny" });
|
|
53
|
+
const method = x402({ vault, wallet: "vault:wallet", payTo: { "api.example": PAY_TO }, fetch: api.fetch });
|
|
54
|
+
const pay = createFoxpay({ gate, store, methods: { x402: method } });
|
|
55
|
+
|
|
56
|
+
const asked = await pay.request({
|
|
57
|
+
merchant: "api.example",
|
|
58
|
+
amount: 10_000, // 0.01 test USDC
|
|
59
|
+
currency: "USDC",
|
|
60
|
+
reason: "Weather data, one call",
|
|
61
|
+
method: "x402",
|
|
62
|
+
idempotencyKey: "call-0001",
|
|
63
|
+
target: { url: "https://api.example/weather" },
|
|
64
|
+
});
|
|
65
|
+
console.log(asked.status); // ask
|
|
66
|
+
|
|
67
|
+
const [request] = await host.pending();
|
|
68
|
+
console.log(request.text); // the exact action that the human approves
|
|
69
|
+
|
|
70
|
+
const done = await pay.complete(asked.id, await host.approve(asked.requestId));
|
|
71
|
+
console.log(done.status, done.body); // paid sunny
|
|
72
|
+
const again = await pay.request({ merchant: "api.example", amount: 10_000, currency: "USDC", reason: "Weather data, one call", method: "x402", idempotencyKey: "call-0001", target: { url: "https://api.example/weather" } });
|
|
73
|
+
console.log(again.status, api.settled.length); // paid 1
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Use cases
|
|
77
|
+
|
|
78
|
+
| Who | What they build | How foxpay helps |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| A browser agent author (for example foxmate) | An agent that buys things for the user in a budget | The planner proposes a payment. foxpay reads the total from the checkout page, foxgate refuses anything over the cap, and the user approves the exact amount before the card is filled. |
|
|
81
|
+
| A developer of AI tools that call paid APIs | A tool that pays for each API call with x402 | Each `402` answer becomes one intent. foxpay checks the network, the token, and the recipient, then signs one payment with a key that stays in foxvault. |
|
|
82
|
+
| A person with SaaS subscriptions | An agent that renews tools within a monthly limit | The host adds a foxgate grant with a `spendCap` and an `expiresAt` at the end of the month, then adds a new grant for the next month. foxpay refuses a renewal that passes the cap. |
|
|
83
|
+
| A caregiver or a parent | Purchases that a family member or an assistant starts | The person who holds the budget approves each payment in the popup. A virtual card provider can make a single-use card with the approved limit for the one merchant. |
|
|
84
|
+
| A QA engineer | Automated tests of checkout flows on staging | Run real checkouts with test cards and the fake card provider. A retry with the same idempotency key never submits a second order. |
|
|
85
|
+
| An MCP server author | A `pay` tool that an outside agent can call | The server calls `request` and returns the waiting request to the client. Only `complete` with a human's token pays. |
|
|
86
|
+
|
|
87
|
+
## How it works
|
|
88
|
+
|
|
89
|
+
```mermaid
|
|
90
|
+
flowchart TD
|
|
91
|
+
P[Planner intent: merchant, amount, currency, reason, method] --> V{Valid intent?}
|
|
92
|
+
V -- no --> R1[refused: bad-intent]
|
|
93
|
+
V -- yes --> Q[Method quote: the host reads the amount and the payee]
|
|
94
|
+
Q -- different amount or currency --> R2[refused: amount-mismatch or currency-mismatch]
|
|
95
|
+
Q --> G{foxgate check: pay action}
|
|
96
|
+
G -- over the cap, wrong currency, no grant --> R3[refused: spend-cap, currency, no-grant]
|
|
97
|
+
G -- ask --> H[Human sees merchant, amount, currency, reason, payee]
|
|
98
|
+
H -- reject --> R4[refused: rejected]
|
|
99
|
+
H -- approve: token --> C{Method check: same page, same total, vault open?}
|
|
100
|
+
C -- no --> R5[refused before the token is used]
|
|
101
|
+
C -- yes --> T{foxgate redeem: one time}
|
|
102
|
+
T --> M[Method pays: card fill or x402]
|
|
103
|
+
M --> RC[Receipt: paid, submitted, unsettled, or failed]
|
|
104
|
+
RC --> E[onEvent: foxtrail-compatible entry]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
1. `request` checks the intent. The idempotency key binds it: a retry with
|
|
108
|
+
the same key gets the same request, or the same receipt after payment.
|
|
109
|
+
2. The method makes a quote on the host side. Card fill reads the total from
|
|
110
|
+
the top document of the merchant tab. x402 reads the `PAYMENT-REQUIRED`
|
|
111
|
+
header. When the planner amount or currency is different, foxpay refuses.
|
|
112
|
+
It never converts currencies.
|
|
113
|
+
3. foxpay builds a foxgate `pay` action from the quote. For x402, the action
|
|
114
|
+
also names the URL and the `402` resource URL in `details`. The `amount` function
|
|
115
|
+
that `payTools()` registers reads the quoted amount, so the planner cannot
|
|
116
|
+
set it. foxgate refuses an amount over the cap before it asks anyone.
|
|
117
|
+
4. The human approves the exact action. foxgate signs a one-time token.
|
|
118
|
+
5. `complete` runs the method check first: the same page, the same total,
|
|
119
|
+
and an open vault. Then foxgate uses the token and counts the spend.
|
|
120
|
+
6. The method pays one time. An intent that was paying when the app stopped
|
|
121
|
+
is never paid again by foxpay. Its outcome is `outcome-unknown`.
|
|
122
|
+
|
|
123
|
+
```mermaid
|
|
124
|
+
sequenceDiagram
|
|
125
|
+
participant A as Agent
|
|
126
|
+
participant F as foxpay
|
|
127
|
+
participant S as Paid API
|
|
128
|
+
participant G as foxgate
|
|
129
|
+
participant U as Human
|
|
130
|
+
participant V as foxvault
|
|
131
|
+
A->>F: request(intent with target.url)
|
|
132
|
+
F->>S: GET url
|
|
133
|
+
S-->>F: 402, PAYMENT-REQUIRED (Base Sepolia, USDC, payTo, amount)
|
|
134
|
+
F->>F: Check network, asset, recipient, token domain, amount
|
|
135
|
+
F->>G: check(pay action)
|
|
136
|
+
G-->>U: Approval request
|
|
137
|
+
U->>G: Approve
|
|
138
|
+
G-->>F: One-time token
|
|
139
|
+
F->>G: redeem(token)
|
|
140
|
+
F->>V: use(wallet handle): sign EIP-3009 authorization, new 32-byte nonce
|
|
141
|
+
F->>S: GET url, PAYMENT-SIGNATURE (one time)
|
|
142
|
+
S-->>F: 200, PAYMENT-RESPONSE (success, transaction)
|
|
143
|
+
F-->>A: Receipt paid, or unsettled when there is no settlement
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Every failure mode has a test: see [docs/failure-modes.md](docs/failure-modes.md).
|
|
147
|
+
|
|
148
|
+
### The x402 signature
|
|
149
|
+
|
|
150
|
+
foxpay signs EIP-3009 `TransferWithAuthorization` with EIP-712, for the x402
|
|
151
|
+
`exact` scheme on EVM. [`src/eip712.ts`](src/eip712.ts) has about 120 lines
|
|
152
|
+
for this one struct. It uses [@noble/curves](https://github.com/paulmillr/noble-curves)
|
|
153
|
+
and [@noble/hashes](https://github.com/paulmillr/noble-hashes) (MIT,
|
|
154
|
+
audited). The signature domain is `USDC`, version `2`, chain 84532, and the
|
|
155
|
+
Base Sepolia USDC contract. The tests compare the digest and the signature
|
|
156
|
+
with [viem](https://viem.sh), which is a dev dependency only. The official
|
|
157
|
+
`@x402/evm` client (Apache-2.0) depends on viem and zod. foxpay needs one
|
|
158
|
+
struct, so it keeps the bundle small and the signing code short enough to read.
|
|
159
|
+
|
|
160
|
+
## API
|
|
161
|
+
|
|
162
|
+
This package is a library only. It has no CLI and no MCP server. A payment
|
|
163
|
+
needs the approval state in foxgate and the secrets in foxvault, and both live
|
|
164
|
+
in one process. A command that starts and stops cannot keep them.
|
|
165
|
+
|
|
166
|
+
### `createFoxpay(options)`
|
|
167
|
+
|
|
168
|
+
| Option | What it does |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `gate` | A foxgate gate made with `createFoxgate({ tools: payTools() })`. Add `[FILL_TOOL]: "fill"` for card fill. |
|
|
171
|
+
| `methods` | `{ name: PayMethod }`, for example `{ card: cardFill(...), x402: x402(...) }`. |
|
|
172
|
+
| `store` | Where intents and receipts live. Use the same store as foxgate, for example `storageAreaStore(browser.storage.local)`. |
|
|
173
|
+
| `onEvent` | `(event) => void \| Promise<void>`. Gets `{ actor: "foxpay", kind, data }`, which fits foxtrail `log.append`. If it throws before a payment, the payment stops with `hook-failed`. |
|
|
174
|
+
| `now` | The clock, in ms. Default: `Date.now`. |
|
|
175
|
+
|
|
176
|
+
| Method | What it does |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `request(intent)` | Checks and quotes the intent and asks foxgate. Returns `{ status: "ask", id, requestId }`, `{ status: "refused", reason, message }`, or a final result when the intent was already paid or the grant needs no approval. |
|
|
179
|
+
| `complete(id, token)` | Pays an approved intent with the foxgate token. Returns `{ status, id, receipt, body? }` or a refusal. Later calls give the same receipt. |
|
|
180
|
+
| `intents()` | Every stored intent with its status. |
|
|
181
|
+
| `receipts()` | Every receipt, oldest first. |
|
|
182
|
+
|
|
183
|
+
An intent is `{ merchant, amount, currency, reason, method, idempotencyKey, target? }`.
|
|
184
|
+
`amount` is whole minor units: cents for USD, atomic units (6 decimals) for
|
|
185
|
+
USDC. `reason` has 1 to 200 characters. `idempotencyKey` has 8 to 128.
|
|
186
|
+
`target` is `{ tabId }` for card fill and `{ url }` for x402.
|
|
187
|
+
|
|
188
|
+
A receipt holds the merchant, the amount, the currency, the reason, the
|
|
189
|
+
method, the payee, the status, the request id, the time, and a `proof`. The
|
|
190
|
+
proof holds only public data: the last 4 card digits, the card handle or the
|
|
191
|
+
provider card id, the page after submit, or the payer, the nonce, the network,
|
|
192
|
+
and the transaction hash.
|
|
193
|
+
|
|
194
|
+
Statuses: `paid` (x402 with a settlement), `submitted` (card checkout
|
|
195
|
+
submitted), `unsettled` (no settlement proof; money may have moved), and
|
|
196
|
+
`failed` (foxpay knows that nothing was paid).
|
|
197
|
+
|
|
198
|
+
Refusal reasons: `bad-intent`, `key-reused`, `amount-mismatch`,
|
|
199
|
+
`currency-mismatch`, `in-progress`, `outcome-unknown`, `not-found`,
|
|
200
|
+
`hook-failed`, `storage-error`, `method-error`, every foxgate deny reason
|
|
201
|
+
(for example `spend-cap`, `currency`, `rejected`, `token-used`), and the
|
|
202
|
+
method reasons below.
|
|
203
|
+
|
|
204
|
+
### `cardFill(options)`
|
|
205
|
+
|
|
206
|
+
| Option | What it does |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `vault`, `browser` | A foxvault vault (with a gate that has a `fill` grant for the merchant) and the WebExtension `browser` object. foxpay pins every fill, also the foxvault one, to the `documentId` that it checked. Needs a foxvault with the `documentId` fill option. |
|
|
209
|
+
| `card` | `{ number, details }`: the handle of the card number and the handle of `"MM/YY CVC"`. |
|
|
210
|
+
| `provider` | Or a `VirtualCardProvider` with `createCard({ intentId, merchant, amount })` that returns `{ id, number, exp, cvc }`, and an optional `cancelCard(id)` that foxpay calls when the payment fails before submit. |
|
|
211
|
+
| `fields` | `{ number, exp, cvc }`: CSS selectors in the checkout page. |
|
|
212
|
+
| `total`, `submit` | The selector of the total text and of the pay button. |
|
|
213
|
+
| `currency` | The merchant currency. foxpay never reads it from the page. |
|
|
214
|
+
| `decimals` | Default: 2. |
|
|
215
|
+
| `allowHttp` | Allow plain `http:` pages. For local tests only. |
|
|
216
|
+
| `waitMs` | How long to wait for the page after submit. Default: 10 seconds. |
|
|
217
|
+
|
|
218
|
+
Reasons: `bad-target`, `no-tab`, `http`, `merchant-mismatch`, `page-changed`,
|
|
219
|
+
`amount-changed`, `no-total`, `no-card`, `domain`, `locked`,
|
|
220
|
+
`provider-error`, `not-found`, and every foxvault fill reason. When a step
|
|
221
|
+
fails after the number fill, foxpay clears the card fields first. When the
|
|
222
|
+
submit script starts but its result is lost, the status is `unsettled` with
|
|
223
|
+
`submit-unknown`, because the order may be placed.
|
|
224
|
+
|
|
225
|
+
### `x402(options)`
|
|
226
|
+
|
|
227
|
+
| Option | What it does |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `vault`, `wallet` | The vault and the handle of a Base Sepolia test wallet key (0x and 64 hex digits). |
|
|
230
|
+
| `payTo` | `{ host: address }`: the recipient that each merchant must name. |
|
|
231
|
+
| `fetch` | Default: the global `fetch`. |
|
|
232
|
+
| `confirm` | `(settlement) => Promise<boolean>`. Check the transaction yourself, for example on an RPC node. When it says no, the receipt is `unsettled`. |
|
|
233
|
+
| `maxTimeoutSeconds` | The longest time a signed authorization stays valid. Default: 300. |
|
|
234
|
+
|
|
235
|
+
Reasons before the payment: `bad-target`, `merchant-mismatch`,
|
|
236
|
+
`no-recipient`, `no-wallet`, `not-402`, `bad-requirements`,
|
|
237
|
+
`requirements-mismatch`, `amount-changed`, `locked`, and `sign-error`
|
|
238
|
+
(status `failed`). foxpay does not follow redirects.
|
|
239
|
+
|
|
240
|
+
After foxpay sends the signed payload, the payee holds a valid authorization
|
|
241
|
+
until `validBefore`. So x402 never returns `failed` after that point. The
|
|
242
|
+
status is `paid` with a settlement, or else `unsettled` with a reason:
|
|
243
|
+
`no-response`, `read-error`, `no-settlement`, `not-confirmed`,
|
|
244
|
+
`http-<status>`, or the payee's `errorReason`. A payee refusal is not proof
|
|
245
|
+
that no money moved. Check an `unsettled` payment on the chain before you
|
|
246
|
+
pay again.
|
|
247
|
+
|
|
248
|
+
### Write your own method
|
|
249
|
+
|
|
250
|
+
A `PayMethod` has `quote(intent)`, an optional `check(context)`, and
|
|
251
|
+
`pay(context)`.
|
|
252
|
+
|
|
253
|
+
- `quote` reads the amount and the payee on the host side. Throw
|
|
254
|
+
`PayRefusal` to refuse. Nothing has been approved yet.
|
|
255
|
+
- `check` runs before foxgate uses the token. Return a reason, for example
|
|
256
|
+
`amount-changed` or `locked`, to stop with no spend.
|
|
257
|
+
- `pay` returns `{ status, reason?, proof?, body? }`. Return `failed` only
|
|
258
|
+
when you know that no money moved. When you do not know, return
|
|
259
|
+
`unsettled`.
|
|
260
|
+
- From `pay`, throw `PayRefusal` only before any money can move. foxpay
|
|
261
|
+
records it as `failed` with its reason. foxpay records any other error
|
|
262
|
+
from `pay` as `unsettled` with `method-error`, because it can come after
|
|
263
|
+
money moved. The error message is not kept.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
| Export | What it does |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `payTools()`, `PAY_TOOL` | The foxgate tool registry entry `foxpay.pay`, with an amount function that reads the quoted amount. |
|
|
270
|
+
| `GATE_CURRENCY` | `{ USDC: "XTS" }`. foxgate takes 3-letter codes, so test USDC counts as XTS, the ISO 4217 code for tests. |
|
|
271
|
+
| `PayRefusal` | Throw it from your own `PayMethod` to refuse with a reason. See "Write your own method". |
|
|
272
|
+
| `BASE_SEPOLIA` | The network, chain id, USDC contract, and token domain that x402 accepts. |
|
|
273
|
+
| `encodeHeader`, `decodeHeader` | Base64 JSON for the x402 headers. |
|
|
274
|
+
| `signAuthorization`, `recoverAuthorizer`, `authorizationDigest`, `addressOf`, `checksumAddress` | The EIP-712 and EIP-3009 helpers. |
|
|
275
|
+
| `foxpay/testing`: `fakeX402Api(options)` | A fake paid API with a local fake facilitator. It checks the signature and refuses a used nonce. For tests and demos only. |
|
|
276
|
+
|
|
277
|
+
### Demo extension
|
|
278
|
+
|
|
279
|
+
`extension/` is a demo for Firefox 153+. The popup shows the spend caps and
|
|
280
|
+
what is left, the payments that wait for approval with their exact action
|
|
281
|
+
JSON, and the receipts. A simulated agent pays for a
|
|
282
|
+
[foxbench](https://github.com/pooriaarab/foxbench) Trailhead Supply checkout
|
|
283
|
+
with a test card, or calls a local fake x402 API.
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
pnpm install
|
|
287
|
+
pnpm e2e # builds, loads the demo in Firefox, and runs the E2E checks
|
|
288
|
+
pnpm build:ext # builds dist-ext/; load it from about:debugging
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`pnpm e2e` writes `artifacts/e2e-<date>.json`. It checks a $26.00 order under
|
|
292
|
+
a $100 cap with one approval (the foxbench server sees the order and card
|
|
293
|
+
ending 4242), a $500 order that is refused before any fill, and an x402 call
|
|
294
|
+
that pays one time while a replay is refused.
|
|
295
|
+
|
|
296
|
+
## Firefox APIs used
|
|
297
|
+
|
|
298
|
+
| API | MDN | Why |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `webNavigation.getFrame` | [getFrame](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation/getFrame) | Read the URL and the `documentId` of the top document of the checkout tab. |
|
|
301
|
+
| `scripting.executeScript` with `documentIds` (Firefox 153+) | [executeScript](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript) | Read the total, fill the expiry and the CVC, and submit, in that exact document only. |
|
|
302
|
+
| `storage.local` | [storage.local](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local) | Keep intents, receipts, foxgate state, and foxvault ciphertexts. |
|
|
303
|
+
| IndexedDB (through foxvault) | [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) | Keep the non-extractable vault key. |
|
|
304
|
+
| `fetch` with host permissions | [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) | Call the paid API and read the x402 headers. |
|
|
305
|
+
| `crypto.getRandomValues`, `crypto.randomUUID` | [getRandomValues](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) | Make the x402 nonce and the virtual card handles. |
|
|
306
|
+
| `btoa`, `atob`, `TextEncoder`, `TextDecoder` | [btoa](https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa) | Encode the x402 headers as base64 JSON. |
|
|
307
|
+
| `publicSuffix.getDomain` (Firefox 153+) | [publicSuffix](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/publicSuffix) | foxgate and foxvault refuse `*.` patterns on a public suffix. Demo only. |
|
|
308
|
+
| `tabs.query` | [tabs.query](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/query) | Find the checkout tab of the merchant. Demo only. |
|
|
309
|
+
| `runtime.sendMessage`, `runtime.onMessage` | [runtime](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime) | The popup talks to the background page. Demo only. |
|
|
310
|
+
| `action` (`default_popup`) | [action](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/action) | The toolbar button that opens the approval popup. Demo only. |
|
|
311
|
+
|
|
312
|
+
## Limits
|
|
313
|
+
|
|
314
|
+
- x402 pays on Base Sepolia only. The network and the USDC contract are
|
|
315
|
+
fixed in the code. There is no mainnet option.
|
|
316
|
+
- foxpay was tested with its own fake facilitator only. It follows the x402
|
|
317
|
+
version 2 HTTP headers, but no test ran against the public x402.org
|
|
318
|
+
facilitator or a real chain.
|
|
319
|
+
- There is no real virtual card provider yet. `VirtualCardProvider` is the
|
|
320
|
+
interface for one, such as Stripe Issuing or Privacy.com. The tests use a
|
|
321
|
+
fake provider.
|
|
322
|
+
- Card fill works in the top document only. A card form inside an iframe,
|
|
323
|
+
for example a hosted payment field, cannot be filled.
|
|
324
|
+
- A card payment ends as `submitted`. foxpay sees the page after submit, not
|
|
325
|
+
the charge. The total is what the merchant page shows.
|
|
326
|
+
- Each merchant needs its own selectors for the card fields, the total, and
|
|
327
|
+
the pay button.
|
|
328
|
+
- foxvault values need 8 characters or more, so the expiry and the CVC are
|
|
329
|
+
one secret, `"MM/YY CVC"`. foxpay fills them through `vault.use` with its
|
|
330
|
+
own bundled function, pinned to the same document. `redact` finds that
|
|
331
|
+
pair, not the CVC alone.
|
|
332
|
+
- foxgate counts the spend when the token is used. A payment that fails
|
|
333
|
+
after that still counts against the cap. Before the token, foxpay checks
|
|
334
|
+
the page total or fetches the `402` again, so a changed price is refused
|
|
335
|
+
with no spend.
|
|
336
|
+
- `outcome-unknown` needs a person. foxpay does not check with the merchant
|
|
337
|
+
or the chain if an interrupted payment went through.
|
|
338
|
+
- Run one foxpay object, one foxgate gate, and one vault for each store, in
|
|
339
|
+
the background page. Two objects on the same store can race.
|
|
340
|
+
- Legal and compliance: foxpay is not a payment processor, a wallet service,
|
|
341
|
+
or financial advice. A real card number in a browser extension can bring
|
|
342
|
+
your app into the scope of PCI DSS and your card issuer's terms. Paying
|
|
343
|
+
with real stablecoins can bring money transmission and sanctions rules.
|
|
344
|
+
Get legal advice before you use real money.
|
|
345
|
+
|
|
346
|
+
## Part of the fox primitives
|
|
347
|
+
|
|
348
|
+
```mermaid
|
|
349
|
+
flowchart LR
|
|
350
|
+
foxkit[foxkit] -- template --> foxpay[foxpay]
|
|
351
|
+
foxgate[foxgate] --> foxpay
|
|
352
|
+
foxvault[foxvault] --> foxpay
|
|
353
|
+
foxgate --> foxvault
|
|
354
|
+
foxpay -. onEvent hook .-> foxtrail[foxtrail]
|
|
355
|
+
foxbench[foxbench] -. demo shop .-> foxpay
|
|
356
|
+
foxpay --> foxmate[foxmate]
|
|
357
|
+
click foxkit "https://github.com/pooriaarab/foxkit"
|
|
358
|
+
click foxgate "https://github.com/pooriaarab/foxgate"
|
|
359
|
+
click foxvault "https://github.com/pooriaarab/foxvault"
|
|
360
|
+
click foxpay "https://github.com/pooriaarab/foxpay"
|
|
361
|
+
click foxtrail "https://github.com/pooriaarab/foxtrail"
|
|
362
|
+
click foxbench "https://github.com/pooriaarab/foxbench"
|
|
363
|
+
click foxmate "https://github.com/pooriaarab/foxmate"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
foxpay depends on foxgate for approvals and caps, and on foxvault for the
|
|
367
|
+
card and the wallet key. foxtrail and foxbench are dev dependencies only:
|
|
368
|
+
the tests write events to a foxtrail log, and the E2E test uses the foxbench
|
|
369
|
+
shop.
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
[MIT](LICENSE)
|
package/dist/card.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Vault } from "foxvault";
|
|
2
|
+
import { type PayMethod } from "./intent.js";
|
|
3
|
+
/** The parts of the WebExtension `browser` object that card fill uses. Pass the same object to foxvault. */
|
|
4
|
+
export interface CardBrowser {
|
|
5
|
+
webNavigation: {
|
|
6
|
+
getFrame(details: {
|
|
7
|
+
tabId: number;
|
|
8
|
+
frameId: number;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
url: string;
|
|
11
|
+
documentId?: string;
|
|
12
|
+
} | null | undefined>;
|
|
13
|
+
};
|
|
14
|
+
scripting: {
|
|
15
|
+
executeScript(injection: {
|
|
16
|
+
target: {
|
|
17
|
+
tabId: number;
|
|
18
|
+
documentIds: string[];
|
|
19
|
+
};
|
|
20
|
+
func: (...args: never[]) => unknown;
|
|
21
|
+
args: unknown[];
|
|
22
|
+
}): Promise<{
|
|
23
|
+
result?: unknown;
|
|
24
|
+
}[]>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A card issuer that makes a single-use card for one merchant and one limit, such as Stripe Issuing. */
|
|
28
|
+
export interface VirtualCardProvider {
|
|
29
|
+
createCard(request: {
|
|
30
|
+
intentId: string;
|
|
31
|
+
merchant: string;
|
|
32
|
+
amount: {
|
|
33
|
+
value: number;
|
|
34
|
+
currency: string;
|
|
35
|
+
};
|
|
36
|
+
}): Promise<{
|
|
37
|
+
id: string;
|
|
38
|
+
number: string;
|
|
39
|
+
exp: string;
|
|
40
|
+
cvc: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** Close a card that foxpay made but did not use, for example after a failed fill (C15). */
|
|
43
|
+
cancelCard?(id: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export interface CardFillOptions {
|
|
46
|
+
/** A foxvault vault with a gate that has a fill grant for the merchants, and the same `browser`. */
|
|
47
|
+
vault: Vault;
|
|
48
|
+
browser: CardBrowser;
|
|
49
|
+
/** A stored card: the handle of the number, and the handle of `"MM/YY CVC"`. */
|
|
50
|
+
card?: {
|
|
51
|
+
number: string;
|
|
52
|
+
details: string;
|
|
53
|
+
};
|
|
54
|
+
/** Or a provider that makes a new card for each payment. */
|
|
55
|
+
provider?: VirtualCardProvider;
|
|
56
|
+
/** CSS selectors in the merchant checkout page. */
|
|
57
|
+
fields: {
|
|
58
|
+
number: string;
|
|
59
|
+
exp: string;
|
|
60
|
+
cvc: string;
|
|
61
|
+
};
|
|
62
|
+
/** The element that holds the total, for example `Total $26.00`. */
|
|
63
|
+
total: string;
|
|
64
|
+
submit: string;
|
|
65
|
+
/** The currency of the merchant. foxpay never reads it from the page. */
|
|
66
|
+
currency: string;
|
|
67
|
+
/** Digits after the decimal point. Default: 2. */
|
|
68
|
+
decimals?: number;
|
|
69
|
+
/** Allow plain http pages. For local tests only. */
|
|
70
|
+
allowHttp?: boolean;
|
|
71
|
+
/** How long to wait for the page after submit. Default: 10 seconds. */
|
|
72
|
+
waitMs?: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function readTotal(selector: string, host: string): string | null;
|
|
75
|
+
export declare function fillDetails(expSelector: string, cvcSelector: string, exp: string, cvc: string, host: string): string;
|
|
76
|
+
export declare function submitCheckout(submitSelector: string, totalSelector: string, totalText: string, host: string): string;
|
|
77
|
+
export declare function clearFields(selectors: string[], host: string): string;
|
|
78
|
+
/** Whole minor units from a total such as `Total $26.00`, or undefined when the text has not exactly one amount. */
|
|
79
|
+
export declare function parseTotal(text: string | null, decimals?: number): number | undefined;
|
|
80
|
+
export declare function cardFill(options: CardFillOptions): PayMethod;
|