@getbags/cash 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ bags --help
13
13
 
14
14
  ### Agent skill (prompt paste)
15
15
 
16
- Hosted skill: https://www.getbags.app/wallet/skill.md
16
+ Hosted skill (agents): https://www.getbags.app/wallet/skill.md
17
17
 
18
18
  Paste into Cursor / Claude / Codex / etc.:
19
19
 
@@ -21,8 +21,9 @@ Paste into Cursor / Claude / Codex / etc.:
21
21
  Set up https://www.getbags.app/wallet/skill.md
22
22
  ```
23
23
 
24
+ Human install guide: https://www.getbags.app/wallet/install
24
25
  Human onboard (bonus + same prompt): https://www.getbags.app/wallet/onboard
25
- Staging: use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL`).
26
+ Staging skill (agents): use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL`).
26
27
 
27
28
  ## Workflow
28
29
 
@@ -39,6 +40,7 @@ Staging: use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL
39
40
  | `search <query>` | NL search across BAGS + ecosystem index |
40
41
  | `check <url>` | Probe schema + exact quote **without paying** |
41
42
  | `fetch <url> [flags]` | Pay and proxy an x402 endpoint |
43
+ | `result <id> [flags]` | Retrieve retained output without another paid call |
42
44
  | `transactions` | Spend / ledger history |
43
45
  | `policies get\|set` | Read or update spend policies |
44
46
  | `keys create\|list\|revoke` | Per-agent wallet keys |
@@ -49,7 +51,7 @@ Staging: use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL
49
51
 
50
52
  `init` flags: `--dry-run`, `--client cursor|claude|codex` (force-create even without a marker directory; repeatable or comma-separated), `--scope project|user` (default `user` — ambient on this machine; use `--scope project` for committable repo-local config). After a successful apply, restart the agent client, then run `bags doctor`.
51
53
 
52
- Fetch / check flags: `-m/--method`, `-b/--body` (JSON string), `--max-price-cents` (fetch), `--format json|pretty`.
54
+ Fetch / check flags: `-m/--method`, `-b/--body` (JSON string), `--max-price-cents` and `--payment-protocol x402|mpp|auto` (fetch), `--format json|pretty` (check). Result flags: `--field`, `--offset`, `--limit`.
53
55
 
54
56
  ## Env
55
57
 
@@ -79,34 +81,39 @@ Token at `~/.bags/credentials.json`. Hosted custody — no local private keys. M
79
81
 
80
82
  ## Publishing `@getbags/cash`
81
83
 
82
- ### Local (preferred dry-run)
84
+ Preferred path is CI via OIDC trusted publishing (no long-lived npm token).
83
85
 
84
- From the repo root:
86
+ ### CI (tag or dispatch)
87
+
88
+ GitHub Action: [`.github/workflows/publish-cash.yml`](../../.github/workflows/publish-cash.yml)
89
+
90
+ 1. Bump `version` in `packages/cli/package.json`
91
+ 2. Tag and push: `git tag cash-v0.3.1 && git push origin cash-v0.3.1` (tag must match package version)
92
+ 3. The workflow builds, tests, and `npm publish`s via GitHub Actions OIDC (`id-token: write`). Configure the trusted publisher once on npmjs.com → `@getbags/cash` → Settings → Trusted publisher (repo `getbagsapp/agentic-payments`, workflow `publish-cash.yml`).
93
+ 4. After publish it retries `npm view` until the version resolves (or fails), and reports whether provenance attestations exist.
94
+
95
+ `workflow_dispatch` defaults to dry-run. To publish manually, always pass an explicit ref (dispatch without `--ref` uses the default branch tip):
85
96
 
86
97
  ```bash
87
- bun run publish:cash --dry-run # install + build + test; never publishes
88
- bun run publish:cash # same as dry-run unless NPM_TOKEN is set
89
- bun run publish:cash --publish # requires NPM_TOKEN; runs npm publish --access public
98
+ gh workflow run publish-cash.yml --ref cash-v0.3.1 -f dry_run=false
90
99
  ```
91
100
 
92
- Or manually:
101
+ `NPM_TOKEN` is an optional emergency fallback only; OIDC is the primary auth. A missing token does not dry-run a real publish attempt — the job fails if OIDC fails and no fallback exists.
102
+
103
+ **Provenance:** OIDC trusted publishing authenticates the publisher; provenance attestations are a separate Sigstore statement. npm does not issue provenance for packages built from a **private** source repository, so `@getbags/cash` will not show a Provenance badge while `getbagsapp/agentic-payments` is private — even though trusted publishing works. Do not add `--provenance` until the source repo is public (npm rejects private-source provenance with HTTP 422). After that cutover, look for the Provenance badge on the version page and a non-404 from `https://registry.npmjs.org/-/npm/v1/attestations/@getbags%2Fcash@<version>`.
104
+
105
+ ### Local dry-run
93
106
 
94
107
  ```bash
95
- cd packages/cli
96
- bun install && bun run build && bun run test
97
- # npm publish --access public # only with npm auth for @getbags
108
+ bun run publish:cash --dry-run # install + build + test; never publishes
98
109
  ```
99
110
 
100
111
  `prepublishOnly` runs the build. Version bumps are manual in `packages/cli/package.json`.
101
112
 
102
- ### CI (tag automation)
113
+ ## Precise payments and retained output
103
114
 
104
- GitHub Action: [`.github/workflows/publish-cash.yml`](../../.github/workflows/publish-cash.yml)
105
-
106
- 1. Bump `version` in `packages/cli/package.json`
107
- 2. Tag and push: `git tag cash-v0.2.0 && git push origin cash-v0.2.0` (tag must match package version)
108
- 3. Workflow builds and tests. It publishes only when repo secret `NPM_TOKEN` is set; otherwise the job succeeds as a dry-run (no npm publish).
115
+ Public `search` and `discover` work without credentials. Paid calls still require a wallet token. Receipts preserve six-decimal USDC and fractional cents. Catalog prices are indicative; check the selected operation's schemas, mode and network before execution. Provider success and target-content success are separate facts.
109
116
 
110
- `workflow_dispatch` defaults to dry-run (build/test only). Set `dry_run=false` to attempt publish (still skips cleanly if `NPM_TOKEN` is missing).
117
+ `bags result <id> --field response.items --offset 0 --limit 20` and the `wallet_result` MCP tool read retained output without another provider call or payment. Results are private to the wallet and expire after 24 hours. Authentication fields are redacted; provider asset URLs may expire earlier. The fetch API and MCP accept `result_mode: "summary"`.
111
118
 
112
- **Do not publish** unless `NPM_TOKEN` is available and you intend a release. Scoped packages typically need an automation token with 2FA bypass for CI. Add the secret under GitHub → Settings → Secrets and variables → Actions (`NPM_TOKEN`).
119
+ MPP supports bounded one-shot `tempo:charge` with explicit chain/currency checks. Hosted session opening is disabled until durable close can be confirmed. `bags sessions list` shows existing session reservations. Hosted `sessions close` currently reports unsupported cleanup and keeps the hold; it does not report the channel closed. Existing channels and reservations remain owned when cleanup fails.
package/SKILL.md CHANGED
@@ -3,13 +3,14 @@ name: bags-cash
3
3
  description: |
4
4
  Pay x402-protected APIs with a BAGS hosted agent wallet (USDC credits, server-side policies).
5
5
  Use when the user has a BAGS invite code, wants to call paid APIs via BAGS Cash / BAGS Wallet,
6
- or mentions bags wallet / bags cash / getbags / x402 micropayments.
6
+ or needs web research, scraping, enrichment, social data, or media generation with no suitable configured tool.
7
+ Prefer the user-selected provider and existing dedicated MCP/API tools; discover paid alternatives only within the user’s task and budget.
7
8
 
8
9
  TRIGGERS: x402, paid API, bags wallet, bags cash, getbags, invite code, redeem, micropayment,
9
- agent wallet, check endpoint, wallet fetch
10
+ agent wallet, check endpoint, wallet fetch, research, scrape, enrich, social data, media generation
10
11
  homepage: https://www.getbags.app/wallet/skill.md
11
12
  metadata:
12
- version: 0.2
13
+ version: 0.3
13
14
  ---
14
15
 
15
16
  # BAGS Cash — Hosted Agent Wallet
@@ -68,7 +69,7 @@ If there is no token yet, redeem (test or invite) first. If live balance is too
68
69
 
69
70
  ## Workflow: balance → discover → check → fetch
70
71
 
71
- Match AgentCash’s agent loop; prefer this order every time.
72
+ Use existing dedicated tools first. Public search/discover works before wallet setup. For paid execution: balance → discover → check → fetch. Discovery is not permission to spend.
72
73
 
73
74
  ### 1. Balance
74
75
 
@@ -84,7 +85,7 @@ npx @getbags/cash discover weather forecast # BAGS first, then open x402 index
84
85
  npx @getbags/cash search "send physical mail" # NL search (catalog + ecosystem)
85
86
  ```
86
87
 
87
- Bare `discover` lists BAGS’ own **$0.01** `GET /api/v1` showcase first — the natural first paid call for a new agent. Prefer `source: bags` results (refund-guaranteed on BAGS merchants when that guarantee is live). Then `source: bazaar`. Default table columns: `url`, `method`, `price`, `merchant`, `source`. Use `--format json` for the raw payload (includes schemas when present).
88
+ Public catalog/search costs nothing. `GET /api/v1` is an optional paid protocol demonstration; use free `/openapi.json` for the API index. Prefer a useful task such as extracting a requested page or finding current weather for the first purchase. Compare method, required inputs, output, price ceiling, wallet mode and source coverage before selecting a provider. A catalog price is indicative; unknown pricing does not mean free. Use `--format json` to inspect schemas, freshness and source failures. Schema coverage is partial or unknown, never a guarantee of successful input/output conformance.
88
89
 
89
90
  ### 3. Check (exact quote, no payment)
90
91
 
@@ -103,11 +104,11 @@ npx @getbags/cash fetch '<url>' -m POST -b '{"key":"value"}'
103
104
  npx @getbags/cash fetch '<url>' --max-price-cents 25
104
105
  ```
105
106
 
106
- Payment receipt (amount, tx hash, network) → stderr; upstream body → stdout (pipe-friendly).
107
+ Payment receipt (exact amount, tx hash, network) → stderr; upstream body → stdout (pipe-friendly). Receipts retain `amount_atomic` (six-decimal USDC) and fractional `amount_cents`. Network fees are platform-paid and marked unmeasured unless observed; do not infer total on-chain debit from the service amount. MPP supports one-shot `tempo:charge` on permitted USDC.e chains. Hosted MPP sessions are unavailable until durable close can be confirmed.
107
108
 
108
109
  **Latency:** wallet fetch often takes **~30–60 seconds** (paid ~30s; free/SIWX ~40–50s). Set agent-framework HTTP timeouts accordingly — do not treat a slow response as a hang until well past 60s.
109
110
 
110
- **Response envelope:** check top-level `status` / `code` first. `status: "success"` means the fetch completed as intended (including free/SIWX calls with `payment.paid: false`). A paid attempt that did not complete returns `status: "error"` with `PRE_PAYMENT_FAILED` or `NETWORK_NOT_ALLOWED` (nested `data.http_status` / `data.payment` keep the upstream diagnostic). Do not treat HTTP 200 alone as “paid OK” — and do not treat nested `http_status: 402` under `status: "success"` as a thing that happens anymore.
111
+ **Response envelope:** check top-level `status` / `code` first. `status: "success"` means the HTTP fetch completed (including free/SIWX calls with `payment.paid: false`). A paid attempt that did not complete returns `status: "error"` with `PRE_PAYMENT_FAILED` or `NETWORK_NOT_ALLOWED` (nested `data.http_status` / `data.payment` keep the upstream diagnostic). Do not treat HTTP 200 alone as “paid OK” — and do not treat nested `http_status: 402` under `status: "success"` as a thing that happens anymore.
111
112
 
112
113
  ## Other commands
113
114
 
@@ -150,3 +151,13 @@ Env: `BAGS_API_URL` (default `https://www.getbags.app`; staging example `https:/
150
151
  Token path: `~/.bags/credentials.json`.
151
152
 
152
153
  MCP: wallet tools on `https://www.getbags.app/api/mcp/mcp` — pass `Authorization: Bearer bag_agent_sk_...` or a `wallet_token` argument on each tool.
154
+
155
+ ## Results and safe retries
156
+
157
+ Read a retained result with `bags result <id> --field response.items --offset 0 --limit 20`, or the read-only `wallet_result` MCP tool. Neither performs another paid call.
158
+
159
+ An upstream 200 can contain target-page 404 or unusable output. Inspect `outcomes` and the payload before claiming task success. Provider failures may still be billed. BAGS-network upstream 5xx may receive wallet credit; external provider billing rules apply. Unknown/submitted payment retains the reservation for reconciliation.
160
+
161
+ Reuse `Idempotency-Key` for retries (24-hour replay); use a new key for an intentional new paid call. Fetch supports `result_mode: "summary"` through the API. `data.result.retrieval_url` retrieves retained output for 24 hours without paying again, with optional `field`, `offset` and `limit`. A result handle is not an async provider poll: accepted provider runs require their provider-specific status contract, which may be paid. Do not blindly execute again. Authentication fields are redacted in retained exports; signed asset URLs may expire before result retention ends.
162
+
163
+ For a frequent origin/task, save only a thin pointer: task, origin and the current search/check workflow. Refresh live schemas and prices on use; never copy credentials or freeze endpoint contracts into a reusable skill.
package/dist/api.d.ts CHANGED
@@ -9,6 +9,10 @@ export type RedeemResponse = {
9
9
  expires: null;
10
10
  };
11
11
  export type BalanceResponse = {
12
+ wallet_id?: string;
13
+ mode?: 'live' | 'test';
14
+ credential_scope?: string;
15
+ payment_readiness?: string;
12
16
  balance_cents: number;
13
17
  daily_spent_cents: number;
14
18
  daily_cap_cents: number;
@@ -32,6 +36,7 @@ export type DiscoverEndpoint = {
32
36
  };
33
37
  };
34
38
  export type DiscoverResponse = {
39
+ coverage?: unknown;
35
40
  endpoints: DiscoverEndpoint[];
36
41
  query?: string | null;
37
42
  };
@@ -40,8 +45,16 @@ export type FetchPayment = {
40
45
  amount_cents: number;
41
46
  tx_hash: string;
42
47
  network: string;
48
+ input_schema_declared?: boolean;
49
+ upstream_4xx?: boolean;
43
50
  };
44
51
  export type FetchResponse = {
52
+ result?: unknown;
53
+ outcomes?: unknown;
54
+ billing?: unknown;
55
+ wallet_id?: string;
56
+ mode?: string;
57
+ request_id?: string;
45
58
  status: string;
46
59
  http_status: number;
47
60
  payment: FetchPayment | null;
@@ -123,6 +136,7 @@ export declare function walletFetch(token: string, input: {
123
136
  body?: unknown;
124
137
  max_price_cents?: number;
125
138
  payment_protocol?: 'x402' | 'mpp' | 'auto';
139
+ result_mode?: 'inline' | 'summary';
126
140
  }): Promise<FetchResponse>;
127
141
  export type MppSessionSummary = {
128
142
  id: string;
@@ -168,3 +182,10 @@ export declare function registerOrigin(token: string, origin: string, opts?: {
168
182
  label?: string;
169
183
  description?: string;
170
184
  }): Promise<Record<string, unknown>>;
185
+ /** Read an owned result; never starts another provider call or payment. */
186
+ export declare function getResult(token: string, input: {
187
+ id: string;
188
+ field?: string;
189
+ offset?: number;
190
+ limit?: number;
191
+ }): Promise<Record<string, unknown>>;
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getBaseUrl } from './config.js';
2
- import { formatApiError } from './errors.js';
2
+ import { formatApiError, withUpstreamDetail } from './errors.js';
3
3
  export class CliApiError extends Error {
4
4
  code;
5
5
  httpStatus;
@@ -50,7 +50,7 @@ export async function apiRequest(path, options = {}) {
50
50
  if (!res.ok || json.status === 'error') {
51
51
  const code = typeof json.code === 'string' ? json.code : res.status === 404 ? 'NOT_FOUND' : `HTTP_${res.status}`;
52
52
  const message = typeof json.message === 'string' ? json.message : res.statusText || 'request failed';
53
- throw new CliApiError(code, message, res.status);
53
+ throw new CliApiError(code, withUpstreamDetail(message, json.data), res.status);
54
54
  }
55
55
  return json;
56
56
  }
@@ -77,6 +77,10 @@ export async function getBalance(token) {
77
77
  ? body.wallet_status
78
78
  : 'unknown';
79
79
  return {
80
+ ...(data.wallet_id ? { wallet_id: data.wallet_id } : {}),
81
+ ...(data.mode ? { mode: data.mode } : {}),
82
+ ...(data.credential_scope ? { credential_scope: data.credential_scope } : {}),
83
+ ...(data.payment_readiness ? { payment_readiness: data.payment_readiness } : {}),
80
84
  balance_cents: Number(data.balance_cents),
81
85
  daily_spent_cents: Number(data.daily_spent_cents),
82
86
  daily_cap_cents: Number(data.daily_cap_cents),
@@ -89,6 +93,7 @@ export async function discover(token, query) {
89
93
  const body = await apiRequest(path, { token });
90
94
  const data = unwrapPayload(body);
91
95
  return {
96
+ ...(data.coverage ? { coverage: data.coverage } : {}),
92
97
  endpoints: Array.isArray(data.endpoints) ? data.endpoints : [],
93
98
  query: data.query,
94
99
  };
@@ -98,6 +103,7 @@ export async function search(token, query) {
98
103
  const body = await apiRequest(path, { token });
99
104
  const data = unwrapPayload(body);
100
105
  return {
106
+ ...(data.coverage ? { coverage: data.coverage } : {}),
101
107
  endpoints: Array.isArray(data.endpoints) ? data.endpoints : [],
102
108
  query: typeof data.query === 'string' ? data.query : query,
103
109
  };
@@ -118,6 +124,7 @@ export async function walletFetch(token, input) {
118
124
  });
119
125
  const data = unwrapPayload(body);
120
126
  return {
127
+ ...data,
121
128
  status: typeof data.status === 'string' ? data.status : 'success',
122
129
  http_status: Number(data.http_status),
123
130
  payment: data.payment ?? null,
@@ -236,3 +243,14 @@ export async function registerOrigin(token, origin, opts = {}) {
236
243
  });
237
244
  return unwrapPayload(body);
238
245
  }
246
+ /** Read an owned result; never starts another provider call or payment. */
247
+ export async function getResult(token, input) {
248
+ const query = new URLSearchParams({ id: input.id });
249
+ if (input.field)
250
+ query.set('field', input.field);
251
+ if (input.offset !== undefined)
252
+ query.set('offset', String(input.offset));
253
+ if (input.limit !== undefined)
254
+ query.set('limit', String(input.limit));
255
+ return unwrapPayload(await apiRequest(`/api/wallet/results?${query}`, { token }));
256
+ }
package/dist/errors.d.ts CHANGED
@@ -1,2 +1,8 @@
1
1
  export declare const ERROR_REMEDIES: Record<string, string>;
2
2
  export declare function formatApiError(code: string, message: string): string;
3
+ /**
4
+ * Surface the wallet API's nested upstream diagnostic (data.http_status +
5
+ * data.response) so PRE_PAYMENT_FAILED failures are legible from the CLI
6
+ * instead of requiring a raw API call to see why a merchant rejected payment.
7
+ */
8
+ export declare function withUpstreamDetail(message: string, data: unknown): string;
package/dist/errors.js CHANGED
@@ -26,3 +26,21 @@ export function formatApiError(code, message) {
26
26
  const line = `${code}: ${message}`;
27
27
  return remedy ? `${line}\n${remedy}` : line;
28
28
  }
29
+ /**
30
+ * Surface the wallet API's nested upstream diagnostic (data.http_status +
31
+ * data.response) so PRE_PAYMENT_FAILED failures are legible from the CLI
32
+ * instead of requiring a raw API call to see why a merchant rejected payment.
33
+ */
34
+ export function withUpstreamDetail(message, data) {
35
+ if (!data || typeof data !== 'object' || Array.isArray(data))
36
+ return message;
37
+ const record = data;
38
+ const status = record.http_status;
39
+ const response = record.response;
40
+ if (status === undefined && response === undefined)
41
+ return message;
42
+ const body = typeof response === 'string' ? response : response ? JSON.stringify(response) : '';
43
+ const head = status !== undefined ? `upstream http ${String(status)}` : 'upstream';
44
+ const detail = body ? `${head}: ${body.slice(0, 300)}` : head;
45
+ return `${message} (${detail})`;
46
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Fetch output diagnostics for failed paid attempts.
3
+ *
4
+ * A paid call can settle on-chain and then hit a merchant 4xx (e.g. a 422 for
5
+ * a bad request param) — money gone with nothing to show. When the 402
6
+ * challenge declared a bazaar input schema, point the agent at `bags check`
7
+ * so the retry carries the required fields.
8
+ */
9
+ export declare const INPUT_SCHEMA_HINT = "this endpoint declares an input schema - run `bags check` to see required fields";
10
+ /** True when payment settled but the upstream response was an error status. */
11
+ export declare function isFailedPaidAttempt(payment: {
12
+ paid?: boolean;
13
+ } | null | undefined, httpStatus: number): boolean;
14
+ /** Should `bags fetch` print the input-schema hint for this fetch result? */
15
+ export declare function shouldPrintInputSchemaHint(payment: {
16
+ paid?: boolean;
17
+ input_schema_declared?: boolean;
18
+ } | null | undefined, httpStatus: number): boolean;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Fetch output diagnostics for failed paid attempts.
3
+ *
4
+ * A paid call can settle on-chain and then hit a merchant 4xx (e.g. a 422 for
5
+ * a bad request param) — money gone with nothing to show. When the 402
6
+ * challenge declared a bazaar input schema, point the agent at `bags check`
7
+ * so the retry carries the required fields.
8
+ */
9
+ export const INPUT_SCHEMA_HINT = 'this endpoint declares an input schema - run `bags check` to see required fields';
10
+ /** True when payment settled but the upstream response was an error status. */
11
+ export function isFailedPaidAttempt(payment, httpStatus) {
12
+ return payment?.paid === true && httpStatus >= 400;
13
+ }
14
+ /** Should `bags fetch` print the input-schema hint for this fetch result? */
15
+ export function shouldPrintInputSchemaHint(payment, httpStatus) {
16
+ return isFailedPaidAttempt(payment, httpStatus) && payment?.input_schema_declared === true;
17
+ }
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { checkEndpoint, createDeposit, createKey, discover, getBalance, getFund, getPolicies, getTransactions, listKeys, redeem, revokeKey, registerOrigin, search, setPolicies, listSessions, closeSessions, walletFetch, CliApiError, } from './api.js';
3
- import { getBaseUrl, requireToken, saveCredentials } from './config.js';
2
+ import { checkEndpoint, createDeposit, createKey, discover, getBalance, getResult, getFund, getPolicies, getTransactions, listKeys, redeem, revokeKey, registerOrigin, search, setPolicies, listSessions, closeSessions, walletFetch, CliApiError, } from './api.js';
3
+ import { getBaseUrl, loadCredentials, requireToken, saveCredentials } from './config.js';
4
+ import { INPUT_SCHEMA_HINT, shouldPrintInputSchemaHint } from './fetch-hints.js';
4
5
  import { formatDoctorSummary, runDoctor } from './doctor.js';
5
6
  import { runInitCommand } from './install-command.js';
6
7
  import { runStdioServer } from './mcp-server.js';
8
+ import { policyPatchFromSets } from './policies.js';
7
9
  import { parseArgs, USAGE } from './parse.js';
8
10
  function centsToDollars(cents) {
9
- return `$${(cents / 100).toFixed(2)}`;
11
+ return `$${(cents / 100).toFixed(Number.isInteger(cents) ? 2 : 6).replace(/(\.\d{2,}?)0+$/, '$1')}`;
10
12
  }
11
13
  function fail(message) {
12
14
  console.error(message);
@@ -20,7 +22,9 @@ function printDiscoverTable(endpoints) {
20
22
  const rows = endpoints.map((ep) => ({
21
23
  url: ep.url,
22
24
  method: ep.method,
23
- price: typeof ep.price_usd === 'number' ? `$${ep.price_usd.toFixed(2)}` : String(ep.price_usd),
25
+ price: typeof ep.price_usd === 'number'
26
+ ? `$${ep.price_usd.toFixed(6).replace(/(\.\d{2,}?)0+$/, '$1')}`
27
+ : String(ep.price_usd),
24
28
  merchant: ep.merchant_name ?? '',
25
29
  source: ep.source ?? 'bags',
26
30
  }));
@@ -36,28 +40,16 @@ function printDiscoverTable(endpoints) {
36
40
  function printJson(data) {
37
41
  console.log(JSON.stringify(data, null, 2));
38
42
  }
39
- function policyPatchFromSets(sets) {
40
- const patch = {};
41
- for (const [key, value] of Object.entries(sets)) {
42
- const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
43
- const num = Number(value);
44
- patch[camel] =
45
- Number.isFinite(num) && String(num) === value
46
- ? num
47
- : value === 'true'
48
- ? true
49
- : value === 'false'
50
- ? false
51
- : value;
52
- }
53
- return patch;
54
- }
55
43
  async function main() {
56
44
  const parsed = parseArgs(process.argv.slice(2));
57
45
  if (parsed.command === 'help') {
58
46
  console.log(USAGE);
59
47
  return;
60
48
  }
49
+ if (parsed.command === 'usage') {
50
+ console.log(parsed.message);
51
+ return;
52
+ }
61
53
  if (parsed.command === 'error') {
62
54
  fail(parsed.message);
63
55
  }
@@ -88,7 +80,7 @@ async function main() {
88
80
  return;
89
81
  }
90
82
  case 'discover': {
91
- const result = await discover(requireToken(), parsed.query);
83
+ const result = await discover(loadCredentials()?.token ?? '', parsed.query);
92
84
  if (parsed.format === 'json') {
93
85
  printJson(result);
94
86
  }
@@ -98,7 +90,7 @@ async function main() {
98
90
  return;
99
91
  }
100
92
  case 'search': {
101
- const result = await search(requireToken(), parsed.query);
93
+ const result = await search(loadCredentials()?.token ?? '', parsed.query);
102
94
  if (parsed.format === 'json') {
103
95
  printJson(result);
104
96
  }
@@ -165,7 +157,11 @@ async function main() {
165
157
  payment_protocol: parsed.paymentProtocol,
166
158
  });
167
159
  if (result.payment?.paid) {
168
- console.error(`payment: ${centsToDollars(result.payment.amount_cents)} | tx ${result.payment.tx_hash} | ${result.payment.network}`);
160
+ const upstreamNote = result.http_status >= 400 ? ` | upstream HTTP ${result.http_status}` : '';
161
+ console.error(`payment: ${centsToDollars(result.payment.amount_cents)} | tx ${result.payment.tx_hash} | ${result.payment.network}${upstreamNote}`);
162
+ if (shouldPrintInputSchemaHint(result.payment, result.http_status)) {
163
+ console.error(INPUT_SCHEMA_HINT);
164
+ }
169
165
  }
170
166
  else if (result.payment) {
171
167
  console.error(`payment: not charged | tx ${result.payment.tx_hash || 'n/a'} | ${result.payment.network || 'n/a'}`);
@@ -229,6 +225,10 @@ async function main() {
229
225
  }
230
226
  return;
231
227
  }
228
+ case 'result': {
229
+ printJson(await getResult(requireToken(), parsed));
230
+ return;
231
+ }
232
232
  case 'transactions': {
233
233
  try {
234
234
  const result = await getTransactions(requireToken(), {
@@ -2,6 +2,12 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type CheckResponse, type DiscoverResponse, type FetchResponse, type BalanceResponse, type PoliciesResponse, type TransactionsResponse, type RedeemResponse, type MppSessionSummary } from './api.js';
3
3
  export declare const WALLET_MCP_TOOL_NAMES: readonly ["wallet_balance", "wallet_discover", "wallet_search", "wallet_check_endpoint", "wallet_fetch", "wallet_sessions_list", "wallet_sessions_close", "wallet_sessions_tick", "wallet_redeem", "wallet_transactions", "wallet_get_settings", "wallet_update_settings"];
4
4
  export type WalletMcpApi = {
5
+ getResult: (token: string, input: {
6
+ id: string;
7
+ field?: string;
8
+ offset?: number;
9
+ limit?: number;
10
+ }) => Promise<Record<string, unknown>>;
5
11
  getBalance: (token: string) => Promise<BalanceResponse>;
6
12
  discover: (token: string, query?: string) => Promise<DiscoverResponse>;
7
13
  search: (token: string, query: string) => Promise<DiscoverResponse>;
@@ -17,6 +23,7 @@ export type WalletMcpApi = {
17
23
  body?: unknown;
18
24
  max_price_cents?: number;
19
25
  payment_protocol?: 'x402' | 'mpp' | 'auto';
26
+ result_mode?: 'inline' | 'summary';
20
27
  }) => Promise<FetchResponse>;
21
28
  listSessions: (token: string) => Promise<{
22
29
  sessions: MppSessionSummary[];
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { z } from 'zod';
4
- import { CliApiError, apiRequest, checkEndpoint, closeSessions, discover, getBalance, getPolicies, getTransactions, listSessions, redeem, search, setPolicies, walletFetch, } from './api.js';
5
+ import { CliApiError, apiRequest, checkEndpoint, closeSessions, discover, getBalance, getResult, getPolicies, getTransactions, listSessions, redeem, search, setPolicies, walletFetch, } from './api.js';
5
6
  import { loadCredentials, saveCredentials as defaultSaveCredentials } from './config.js';
6
7
  import { WALLET_MCP_INSTRUCTIONS } from './mcp-instructions.js';
7
8
  export const WALLET_MCP_TOOL_NAMES = [
@@ -47,6 +48,7 @@ function missingTokenResult() {
47
48
  function resolveApi(partial) {
48
49
  return {
49
50
  getBalance,
51
+ getResult,
50
52
  discover,
51
53
  search,
52
54
  checkEndpoint,
@@ -73,7 +75,8 @@ function resolveToken(deps) {
73
75
  export function createWalletMcpServer(deps = {}) {
74
76
  const api = resolveApi(deps.api);
75
77
  const persistCredentials = deps.saveCredentials ?? defaultSaveCredentials;
76
- const server = new McpServer({ name: 'bags-cash', version: '0.3.0' }, { instructions: WALLET_MCP_INSTRUCTIONS });
78
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
79
+ const server = new McpServer({ name: 'bags-cash', version }, { instructions: WALLET_MCP_INSTRUCTIONS });
77
80
  async function withToken(run) {
78
81
  const token = resolveToken(deps);
79
82
  if (!token)
@@ -104,10 +107,11 @@ export function createWalletMcpServer(deps = {}) {
104
107
  query: z.string().optional().describe('Optional search keywords'),
105
108
  },
106
109
  annotations: { readOnlyHint: true, openWorldHint: true },
107
- }, async (args) => withToken(async (token) => {
110
+ }, async (args) => (async () => {
111
+ const token = resolveToken(deps) ?? '';
108
112
  const q = typeof args.query === 'string' ? args.query.trim() : '';
109
113
  return textContent(await api.discover(token, q || undefined));
110
- }));
114
+ })());
111
115
  server.registerTool('wallet_search', {
112
116
  title: 'Search payable endpoints',
113
117
  description: 'Natural-language search for x402 endpoints. BAGS catalog first, best-effort ecosystem (Bazaar) second. Returns price, description, schemas when available.',
@@ -115,7 +119,7 @@ export function createWalletMcpServer(deps = {}) {
115
119
  query: z.string().min(1).describe('Natural-language or keyword query'),
116
120
  },
117
121
  annotations: { readOnlyHint: true, openWorldHint: true },
118
- }, async (args) => withToken(async (token) => textContent(await api.search(token, String(args.query ?? '').trim()))));
122
+ }, async (args) => textContent(await api.search(resolveToken(deps) ?? '', String(args.query ?? '').trim())));
119
123
  server.registerTool('wallet_check_endpoint', {
120
124
  title: 'Check endpoint quote',
121
125
  description: 'Probe an x402 URL for schema + exact price quote without paying (AgentCash check parity). Use before wallet_fetch.',
@@ -130,13 +134,25 @@ export function createWalletMcpServer(deps = {}) {
130
134
  method: typeof args.method === 'string' ? args.method : undefined,
131
135
  body: args.body,
132
136
  }))));
137
+ server.registerTool('wallet_result', {
138
+ title: 'Read retained wallet result',
139
+ description: 'Retrieve full output, fields or array pages from a previous wallet_fetch result for 24 hours. Does not execute or charge again. Provider asset URLs may expire earlier.',
140
+ inputSchema: {
141
+ id: z.string().uuid(),
142
+ field: z.string().max(200).optional(),
143
+ offset: z.number().int().nonnegative().optional(),
144
+ limit: z.number().int().min(1).max(100).optional(),
145
+ },
146
+ annotations: { readOnlyHint: true, openWorldHint: false },
147
+ }, async (args) => withToken(async (token) => textContent(await api.getResult(token, args))));
133
148
  server.registerTool('wallet_fetch', {
134
149
  title: 'Wallet fetch (pay + proxy)',
135
- description: 'Pay for and proxy an x402- or MPP-protected HTTP request using the BAGS hosted wallet. MPP tempo:session channels stay open until wallet_sessions_close.',
150
+ description: 'Pay for an x402 or one-shot MPP tempo:charge HTTP request with the hosted wallet. Check the quote and wallet mode first. Hosted MPP sessions are currently unavailable.',
136
151
  inputSchema: {
137
152
  url: z.string().url(),
138
153
  method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional(),
139
154
  body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
155
+ result_mode: z.enum(['inline', 'summary']).optional(),
140
156
  max_price_cents: z.number().int().positive().optional(),
141
157
  payment_protocol: z
142
158
  .enum(['x402', 'mpp', 'auto'])
@@ -148,6 +164,7 @@ export function createWalletMcpServer(deps = {}) {
148
164
  url: String(args.url),
149
165
  method: typeof args.method === 'string' ? args.method : undefined,
150
166
  body: args.body,
167
+ result_mode: args.result_mode,
151
168
  max_price_cents: typeof args.max_price_cents === 'number' ? args.max_price_cents : undefined,
152
169
  payment_protocol: args.payment_protocol === 'x402' ||
153
170
  args.payment_protocol === 'mpp' ||
@@ -165,7 +182,7 @@ export function createWalletMcpServer(deps = {}) {
165
182
  }, async () => withToken(async (token) => textContent(await api.listSessions(token))));
166
183
  server.registerTool('wallet_sessions_close', {
167
184
  title: 'Close MPP session',
168
- description: 'Close one or all open MPP tempo:session channels (settles reservation to cumulative spend). Pass all=true to close every open session.',
185
+ description: 'Request cleanup of existing MPP sessions. On-chain close is currently unsupported: the call reports failure and preserves the reservation and ownership. New sessions cannot be opened.',
169
186
  inputSchema: {
170
187
  session_id: z.string().uuid().optional(),
171
188
  channel_id: z.string().min(1).optional(),
package/dist/parse.d.ts CHANGED
@@ -2,12 +2,21 @@ import type { ClientId, InstallScope } from './detect.js';
2
2
  export type PaymentProtocol = 'x402' | 'mpp' | 'auto';
3
3
  export type ParsedArgs = {
4
4
  command: 'help';
5
+ } | {
6
+ command: 'usage';
7
+ message: string;
5
8
  } | {
6
9
  command: 'redeem';
7
10
  code?: string;
8
11
  test: boolean;
9
12
  } | {
10
13
  command: 'balance';
14
+ } | {
15
+ command: 'result';
16
+ id: string;
17
+ field?: string;
18
+ offset?: number;
19
+ limit?: number;
11
20
  } | {
12
21
  command: 'discover';
13
22
  format: 'json' | 'pretty';
package/dist/parse.js CHANGED
@@ -1,5 +1,27 @@
1
1
  import { walletDepositsVisible } from './deposits-visible.js';
2
2
  const VALID_CLIENTS = ['cursor', 'claude', 'codex'];
3
+ /**
4
+ * Per-command usage strings - the same text the flag-error paths print, so
5
+ * `bags <cmd> --help` never disagrees with a bad-flag error.
6
+ */
7
+ const COMMAND_USAGE = {
8
+ redeem: 'usage: bags redeem <code> | bags redeem --test',
9
+ balance: 'usage: bags balance',
10
+ result: 'usage: bags result <id> [--field path] [--offset N] [--limit N]',
11
+ discover: 'usage: bags discover [query] [--format json|pretty]',
12
+ search: 'usage: bags search <query> [--format json|pretty]',
13
+ check: 'usage: bags check <url> [flags]',
14
+ fetch: 'usage: bags fetch <url> [flags]',
15
+ sessions: 'usage: bags sessions list|close [id] [--all --yes]',
16
+ init: 'usage: bags init [--dry-run] [--client cursor|claude|codex] [--scope project|user]',
17
+ doctor: 'usage: bags doctor [--scope project|user] [--client cursor|claude|codex]',
18
+ fund: 'usage: bags fund [--open] [--create] [--network NAME]',
19
+ transactions: 'usage: bags transactions [--limit N] [--cursor C] [--format json|pretty]',
20
+ policies: 'usage: bags policies get|set [--key=value ...]',
21
+ keys: 'usage: bags keys create|list|revoke',
22
+ register: 'usage: bags register <origin>',
23
+ mcp: 'usage: bags mcp',
24
+ };
3
25
  function parseClientList(raw) {
4
26
  const parts = raw
5
27
  .split(',')
@@ -48,6 +70,13 @@ export function parseArgs(argv) {
48
70
  return { command: 'help' };
49
71
  }
50
72
  const [cmd, ...rest] = argv;
73
+ // -h/--help on any command prints that command's usage (stdout, exit 0).
74
+ if (rest.includes('-h') || rest.includes('--help')) {
75
+ const known = cmd in COMMAND_USAGE;
76
+ if (!known)
77
+ return { command: 'error', message: `unknown command: ${cmd}` };
78
+ return { command: 'usage', message: COMMAND_USAGE[cmd] };
79
+ }
51
80
  switch (cmd) {
52
81
  case 'redeem': {
53
82
  let code;
@@ -411,6 +440,34 @@ export function parseArgs(argv) {
411
440
  }
412
441
  return { command: 'fund', open, create, network };
413
442
  }
443
+ case 'result': {
444
+ const id = rest[0];
445
+ if (!id || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(id))
446
+ return { command: 'error', message: COMMAND_USAGE.result };
447
+ const out = {
448
+ command: 'result',
449
+ id,
450
+ };
451
+ for (let i = 1; i < rest.length; i += 2) {
452
+ const flag = rest[i], value = rest[i + 1];
453
+ if (!value || !['--field', '--offset', '--limit'].includes(flag))
454
+ return { command: 'error', message: COMMAND_USAGE.result };
455
+ if (flag === '--field')
456
+ out.field = value;
457
+ else {
458
+ const n = Number(value);
459
+ if (!Number.isSafeInteger(n) ||
460
+ n < (flag === '--offset' ? 0 : 1) ||
461
+ (flag === '--limit' && n > 100))
462
+ return { command: 'error', message: COMMAND_USAGE.result };
463
+ if (flag === '--offset')
464
+ out.offset = n;
465
+ else
466
+ out.limit = n;
467
+ }
468
+ }
469
+ return out;
470
+ }
414
471
  case 'transactions': {
415
472
  const parsed = takeFormat(rest, 'usage: bags transactions [--limit N] [--cursor C] [--format json|pretty]');
416
473
  if ('error' in parsed)
@@ -621,7 +678,7 @@ Usage:
621
678
  npx @getbags/cash <command>
622
679
 
623
680
  Workflow: balance → discover|search → check → fetch
624
- MPP streams: fetch (opens session) → sessions close
681
+ MPP: one-shot tempo:charge; new hosted sessions are unavailable
625
682
 
626
683
  Commands:
627
684
  redeem <code> Redeem an invite code and save the wallet token
@@ -631,7 +688,9 @@ ${fundLine} discover [query] List BAGS merchant endpoints (BAGS fi
631
688
  search <query> NL search: BAGS catalog + ecosystem index
632
689
  check <url> Probe an endpoint for schema + exact quote (no pay)
633
690
  fetch <url> [flags] Pay and proxy an x402 / MPP endpoint
634
- sessions list|close List or close open MPP tempo:session channels
691
+ sessions list List existing MPP session reservations
692
+ sessions close <id> Request cleanup; unsupported close preserves the hold
693
+ result <id> [flags] Read retained output without another payment
635
694
  transactions List wallet ledger / spend history
636
695
  policies get|set Read or update spend policies
637
696
  keys create|list|revoke Manage per-agent wallet keys
@@ -661,7 +720,12 @@ Fetch / check flags:
661
720
  --payment-protocol x402|mpp|auto Force rail (default: auto)
662
721
  --format json|pretty Machine or human output
663
722
 
664
- Sessions:
723
+ Result flags:
724
+ --field <path> Select a retained field (e.g. response.items)
725
+ --offset <N> Array offset (default: 0)
726
+ --limit <N> Array page size (1–100; default: 20)
727
+
728
+ Sessions (existing holds only; hosted close currently reports unsupported):
665
729
  npx @getbags/cash sessions list
666
730
  npx @getbags/cash sessions close <id>
667
731
  npx @getbags/cash sessions close --all --yes
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Client-side policy patch construction for `bags policies set`.
3
+ *
4
+ * The wallet policies route validates a snake_case PatchSchema
5
+ * (src/app/api/wallet/policies/route.ts). Zod strips unknown keys there, so a
6
+ * misspelled or wrong-case flag used to return 200 with an unchanged policy —
7
+ * a silent no-op. This module normalizes every accepted spelling to
8
+ * snake_case and rejects unrecognized keys before the request leaves the CLI.
9
+ */
10
+ export declare class PolicyPatchError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /**
14
+ * Normalize a flag key to the server's snake_case spelling.
15
+ * Accepts kebab-case (`per-call-cap-cents`), camelCase (`perCallCapCents`),
16
+ * and snake_case (`per_call_cap_cents`).
17
+ */
18
+ export declare function policyKeyToSnake(key: string): string;
19
+ /**
20
+ * Build the PATCH body for `bags policies set --key=value ...`.
21
+ *
22
+ * Values coerce like before: integer-looking strings become numbers,
23
+ * `true`/`false` become booleans, anything else stays a string. Unknown keys
24
+ * throw (the server would silently strip them and report success).
25
+ */
26
+ export declare function policyPatchFromSets(sets: Record<string, string>): Record<string, unknown>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Client-side policy patch construction for `bags policies set`.
3
+ *
4
+ * The wallet policies route validates a snake_case PatchSchema
5
+ * (src/app/api/wallet/policies/route.ts). Zod strips unknown keys there, so a
6
+ * misspelled or wrong-case flag used to return 200 with an unchanged policy —
7
+ * a silent no-op. This module normalizes every accepted spelling to
8
+ * snake_case and rejects unrecognized keys before the request leaves the CLI.
9
+ */
10
+ /**
11
+ * Exact keys the server PatchSchema accepts. Keep in sync with
12
+ * src/app/api/wallet/policies/route.ts.
13
+ */
14
+ const VALID_POLICY_KEYS = new Set([
15
+ 'per_call_cap_cents',
16
+ 'daily_cap_cents',
17
+ 'weekly_cap_cents',
18
+ 'monthly_cap_cents',
19
+ 'total_cap_cents',
20
+ 'human_approval_threshold_cents',
21
+ 'origin_allowlist',
22
+ 'origin_blocklist',
23
+ 'status',
24
+ ]);
25
+ export class PolicyPatchError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'PolicyPatchError';
29
+ }
30
+ }
31
+ /**
32
+ * Normalize a flag key to the server's snake_case spelling.
33
+ * Accepts kebab-case (`per-call-cap-cents`), camelCase (`perCallCapCents`),
34
+ * and snake_case (`per_call_cap_cents`).
35
+ */
36
+ export function policyKeyToSnake(key) {
37
+ return key
38
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
39
+ .replace(/-/g, '_')
40
+ .toLowerCase();
41
+ }
42
+ /**
43
+ * Build the PATCH body for `bags policies set --key=value ...`.
44
+ *
45
+ * Values coerce like before: integer-looking strings become numbers,
46
+ * `true`/`false` become booleans, anything else stays a string. Unknown keys
47
+ * throw (the server would silently strip them and report success).
48
+ */
49
+ export function policyPatchFromSets(sets) {
50
+ const patch = {};
51
+ for (const [key, value] of Object.entries(sets)) {
52
+ const snake = policyKeyToSnake(key);
53
+ if (!VALID_POLICY_KEYS.has(snake)) {
54
+ throw new PolicyPatchError(`unknown policy key: ${key}. ` +
55
+ `Valid keys: ${[...VALID_POLICY_KEYS].join(', ')}. ` +
56
+ `Example: bags policies set --per-call-cap-cents=100`);
57
+ }
58
+ const num = Number(value);
59
+ patch[snake] =
60
+ Number.isFinite(num) && String(num) === value
61
+ ? num
62
+ : value === 'true'
63
+ ? true
64
+ : value === 'false'
65
+ ? false
66
+ : value;
67
+ }
68
+ return patch;
69
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@getbags/cash",
3
- "version": "0.3.0",
4
- "description": "BAGS Cash CLI — hosted agent wallet for x402 (balance → discover → check → fetch)",
3
+ "version": "0.3.1",
4
+ "description": "BAGS Cash CLI \u2014 hosted agent wallet for x402 (balance \u2192 discover \u2192 check \u2192 fetch)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bags": "dist/index.js"
@@ -39,12 +39,13 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@modelcontextprotocol/sdk": "1.26.0",
43
- "zod": "^4.3.6"
42
+ "@modelcontextprotocol/sdk": "1.32.1",
43
+ "zod": "^4.6.5"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^25.0.3",
47
47
  "typescript": "6.0.2",
48
- "vitest": "^4.0.18"
49
- }
48
+ "vitest": "^5.0.3"
49
+ },
50
+ "packageManager": "bun@1.4.2"
50
51
  }