aeron-wallet 0.2.0 → 0.3.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/README.md +58 -1
- package/dist/history.js +2 -0
- package/dist/outcome.js +71 -0
- package/dist/payer.js +9 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,32 @@ need ETH: the facilitator relays the transaction and pays gas.
|
|
|
37
37
|
| `session revoke <id>` | Kill a session. It stops paying on its next call. |
|
|
38
38
|
| `mcp` | Run as an MCP server over stdio. The default with no arguments. |
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Install it in an agent
|
|
41
|
+
|
|
42
|
+
**Claude Code**
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
/plugin marketplace add aeronlabs/aeron-wallet
|
|
46
|
+
/plugin install aeron-wallet@aeronlabs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Cursor**
|
|
50
|
+
|
|
51
|
+
[](cursor://anysphere.cursor-deeplink/mcp/install?name=aeron-wallet&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImFlcm9uLXdhbGxldCIsIm1jcCJdfQ==)
|
|
52
|
+
|
|
53
|
+
**Gemini CLI**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gemini extensions install https://github.com/aeronlabs/aeron-wallet
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**VS Code**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
code --add-mcp '{"name":"aeron-wallet","command":"npx","args":["-y","aeron-wallet","mcp"]}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Anything else that speaks MCP**
|
|
41
66
|
|
|
42
67
|
```json
|
|
43
68
|
{
|
|
@@ -117,6 +142,31 @@ The wallet refuses to sign above either cap, so a loop cannot drain it.
|
|
|
117
142
|
| `MAX_PER_CALL_USD` | `0.05` | Largest single payment. |
|
|
118
143
|
| `DAILY_CAP_USD` | `1` | Total for the current UTC day. |
|
|
119
144
|
|
|
145
|
+
## What a result means
|
|
146
|
+
|
|
147
|
+
A request that comes back 4xx is not one situation, it is three, and they
|
|
148
|
+
differ in the only way that matters: whether the money left the wallet. The
|
|
149
|
+
signal is the settlement receipt — a service that settled returns
|
|
150
|
+
`X-PAYMENT-RESPONSE` with a transaction hash, and one that did not, does not.
|
|
151
|
+
|
|
152
|
+
| `status` | Charged | What happened |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `settled` | yes | The service answered. `reason` is set only in the bad case below. |
|
|
155
|
+
| `rejected` | no | HTTP 402. The service refused the payment; the authorization is unspent. |
|
|
156
|
+
| `failed` | no | The service returned an error *and declined to charge* — usually its own upstream failed. |
|
|
157
|
+
|
|
158
|
+
The case worth naming: a `settled` row **with** a `reason` means the money
|
|
159
|
+
moved and nothing came back. That is the only outcome where the wallet is out
|
|
160
|
+
of pocket for nothing, so it is reported as itself rather than folded in with
|
|
161
|
+
refusals that cost nothing.
|
|
162
|
+
|
|
163
|
+
Only `settled` counts against `DAILY_CAP_USD`. A refusal and an upstream
|
|
164
|
+
failure leave the balance untouched, so neither eats into the cap.
|
|
165
|
+
|
|
166
|
+
`reason` quotes the service's own message when it gave one, instead of a
|
|
167
|
+
generic phrase — an agent operator reading a log needs to know whether to
|
|
168
|
+
retry, top up, or fix the seller.
|
|
169
|
+
|
|
120
170
|
## Configuration
|
|
121
171
|
|
|
122
172
|
| Variable | Default |
|
|
@@ -128,6 +178,13 @@ The wallet refuses to sign above either cap, so a loop cannot drain it.
|
|
|
128
178
|
| `AERON_WALLET_KEY` | unset. Overrides the stored key. |
|
|
129
179
|
| `AERON_WALLET_SESSION` | unset. Binds the whole process to one session. |
|
|
130
180
|
|
|
181
|
+
## Releases
|
|
182
|
+
|
|
183
|
+
Published from a tag by GitHub Actions using npm trusted publishing, so no
|
|
184
|
+
long-lived npm token exists to leak and every tarball carries a provenance
|
|
185
|
+
attestation: proof of the commit and workflow it was built from. Verify with
|
|
186
|
+
`npm audit signatures` after installing.
|
|
187
|
+
|
|
131
188
|
## Where payments go
|
|
132
189
|
|
|
133
190
|
Payments settle on Robinhood Chain mainnet in USDG through the Aeron
|
package/dist/history.js
CHANGED
|
@@ -30,6 +30,8 @@ export function createHistory(cfg) {
|
|
|
30
30
|
const midnight = new Date(now);
|
|
31
31
|
midnight.setHours(0, 0, 0, 0);
|
|
32
32
|
return readAll()
|
|
33
|
+
// Only 'settled' rows moved money. A refusal and an upstream failure both
|
|
34
|
+
// leave the balance untouched, so neither may eat into the daily cap.
|
|
33
35
|
.filter((r) => r.status === 'settled' && new Date(r.ts) >= midnight)
|
|
34
36
|
.reduce((sum, r) => sum + r.amountUsd, 0);
|
|
35
37
|
},
|
package/dist/outcome.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What actually happened after the wallet handed over a payment.
|
|
3
|
+
*
|
|
4
|
+
* "The request came back 4xx" is not one situation, it is three, and they
|
|
5
|
+
* differ in the only way an agent operator cares about: whether the money
|
|
6
|
+
* left the wallet.
|
|
7
|
+
*
|
|
8
|
+
* - the service answered → paid, got the goods
|
|
9
|
+
* - the service refused the payment → not charged, the authorization stands
|
|
10
|
+
* - the service refused to charge → not charged, its upstream failed
|
|
11
|
+
* - the service charged and failed → charged, got nothing ← say this loudly
|
|
12
|
+
*
|
|
13
|
+
* The signal is the settlement receipt. A service that settles returns
|
|
14
|
+
* X-PAYMENT-RESPONSE with a transaction hash; one that did not, does not.
|
|
15
|
+
*/
|
|
16
|
+
/** The service's own words, dug out of whatever shape it used to say them. */
|
|
17
|
+
export function serviceMessage(body) {
|
|
18
|
+
if (!body.trim())
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(body);
|
|
22
|
+
const error = parsed.error;
|
|
23
|
+
if (typeof error === 'string' && error.trim())
|
|
24
|
+
return error.trim();
|
|
25
|
+
if (error && typeof error === 'object') {
|
|
26
|
+
const message = error.message;
|
|
27
|
+
if (typeof message === 'string' && message.trim())
|
|
28
|
+
return message.trim();
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not JSON. A short plain-text body is still better than a generic phrase.
|
|
34
|
+
const text = body.trim();
|
|
35
|
+
return text.length <= 200 ? text : null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const withMessage = (fallback, body) => {
|
|
39
|
+
const message = serviceMessage(body);
|
|
40
|
+
return message ? `${fallback}: ${message}` : fallback;
|
|
41
|
+
};
|
|
42
|
+
export function describeOutcome(httpStatus, transaction, body) {
|
|
43
|
+
const charged = transaction !== null;
|
|
44
|
+
if (httpStatus < 400)
|
|
45
|
+
return { ok: true, charged, status: 'settled' };
|
|
46
|
+
if (charged) {
|
|
47
|
+
// The worst case and the quietest one: the money moved and the caller has
|
|
48
|
+
// nothing to show for it. Named explicitly so it cannot be mistaken for a
|
|
49
|
+
// refusal that cost nothing.
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
charged: true,
|
|
53
|
+
status: 'settled',
|
|
54
|
+
reason: withMessage(`charged (${transaction}) but the service then returned HTTP ${httpStatus}`, body),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (httpStatus === 402) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
charged: false,
|
|
61
|
+
status: 'rejected',
|
|
62
|
+
reason: withMessage('the service refused the payment; you were not charged', body),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
charged: false,
|
|
68
|
+
status: 'failed',
|
|
69
|
+
reason: withMessage(`the service returned HTTP ${httpStatus} and did not charge you`, body),
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/payer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { hashDomain } from 'viem';
|
|
4
4
|
import { checkSession } from './sessions.js';
|
|
5
|
+
import { describeOutcome } from './outcome.js';
|
|
5
6
|
/** EIP-3009 typed data, mirrored from the facilitator side. */
|
|
6
7
|
const TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
7
8
|
TransferWithAuthorization: [
|
|
@@ -169,8 +170,10 @@ export async function payX402(url, init, deps) {
|
|
|
169
170
|
/* receipt header is informational */
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
// Whether money moved is the settlement receipt's business, not the status
|
|
174
|
+
// code's: a service can refuse to charge and still answer 4xx.
|
|
175
|
+
const outcome = describeOutcome(second.status, transaction, secondBody);
|
|
176
|
+
if (outcome.charged && binding)
|
|
174
177
|
binding.recordSpend(amountUsd);
|
|
175
178
|
history.append({
|
|
176
179
|
ts: new Date().toISOString(),
|
|
@@ -178,11 +181,11 @@ export async function payX402(url, init, deps) {
|
|
|
178
181
|
amountUsd,
|
|
179
182
|
payer: account.address,
|
|
180
183
|
transaction,
|
|
181
|
-
status:
|
|
182
|
-
reason
|
|
184
|
+
status: outcome.status,
|
|
185
|
+
...(outcome.reason ? { reason: outcome.reason } : {}),
|
|
183
186
|
});
|
|
184
187
|
return {
|
|
185
|
-
paid:
|
|
186
|
-
reason
|
|
188
|
+
paid: outcome.ok, status: second.status, amountUsd, transaction, body: secondBody,
|
|
189
|
+
...(outcome.reason ? { reason: outcome.reason } : {}),
|
|
187
190
|
};
|
|
188
191
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aeron-wallet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.aeronlabs/aeron-wallet",
|
|
5
5
|
"description": "Non-custodial agent wallet for Robinhood Chain. Pays x402 requests in USDG. Ships as a CLI and an MCP server.",
|
|
6
6
|
"license": "MIT",
|