Skip to content

Commit 887c530

Browse files
authored
feat: add use-agently web (#57)
* feat: add `use-agently web` * remove -d for get command * always show help after error * resolve comments + use bun api * fix: use bun api * chore: use fetch spy * stream body to file * remove global option from parent command * fix tests
1 parent 83f8d9a commit 887c530

File tree

13 files changed

+1163
-153
lines changed

13 files changed

+1163
-153
lines changed

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ use-agently a2a send --uri "uri/url" -m "message" # Send a message via A2A pr
152152
use-agently a2a card --uri "uri/url" # Fetch and display the A2A agent card
153153
use-agently mcp tools --uri "uri/url" # List tools on an MCP server
154154
use-agently mcp call "tool" ["args"] --uri "uri/url" # Call a tool on an MCP server
155+
use-agently web get <url> # HTTP GET with x402 payment support
156+
use-agently web post <url> # HTTP POST with x402 payment support
157+
use-agently web put <url> # HTTP PUT with x402 payment support
158+
use-agently web patch <url> # HTTP PATCH with x402 payment support
159+
use-agently web delete <url> # HTTP DELETE with x402 payment support
155160
```
156161

157162
### Design Rules for New Commands

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ Resolve an ERC-8004 agent URI and display its details from the Agently marketpla
137137
use-agently erc-8004 --uri eip155:8453/erc-8004:0x1234/1
138138
```
139139

140+
### `web`
141+
142+
Make raw HTTP requests with x402 payment support. Accepts curl-like flags.
143+
144+
```bash
145+
# GET request with verbose output
146+
use-agently web get https://api.example.com/data -v
147+
148+
# POST with JSON body
149+
use-agently web post https://api.example.com/data -d '{"key":"value"}' -H "Content-Type: application/json"
150+
151+
# Dry-run shows cost if payment required; add --pay to authorize
152+
use-agently web get https://paid-api.example.com/resource
153+
```
154+
140155
## How It Works
141156

142157
1. **Wallet**`init` generates an EVM private key stored locally. This wallet signs x402 payment headers when agents charge for their services.

bun.lock

Lines changed: 78 additions & 108 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Custom server entrypoint — overrides the framework's auto-generated server.
2+
import { AixyzServer } from "aixyz/server";
3+
import { useA2A } from "aixyz/server/adapters/a2a";
4+
import { AixyzMCP } from "aixyz/server/adapters/mcp";
5+
import { facilitator } from "./accepts";
6+
import * as agent from "./agent";
7+
import * as freeEcho from "./agents/free-echo";
8+
import * as freeEcho10 from "./agents/free-echo-10";
9+
import * as paidEcho from "./agents/paid-echo";
10+
import * as echoTool from "./tools/echo";
11+
import * as paidEchoTool from "./tools/paid-echo-tool";
12+
import express from "express";
13+
14+
const server = new AixyzServer(facilitator);
15+
await server.initialize();
16+
server.unstable_withIndexPage();
17+
18+
// A2A agents
19+
useA2A(server, agent);
20+
useA2A(server, freeEcho, "free-echo");
21+
useA2A(server, freeEcho10, "free-echo-10");
22+
useA2A(server, paidEcho, "paid-echo");
23+
24+
// MCP tools
25+
const mcp = new AixyzMCP(server);
26+
await mcp.register("echo", echoTool);
27+
await mcp.register("paid-echo-tool", paidEchoTool);
28+
await mcp.connect();
29+
30+
// Plain HTTP test endpoints for `use-agently web` command tests
31+
server.express.use("/http", express.json());
32+
33+
// Free endpoints
34+
server.express.get("/http/free", (_req, res) => {
35+
res.json({ message: "free GET response" });
36+
});
37+
38+
server.express.post("/http/free", (req, res) => {
39+
res.json({ message: "free POST response", body: req.body });
40+
});
41+
42+
server.express.put("/http/free", (req, res) => {
43+
res.json({ message: "free PUT response", body: req.body });
44+
});
45+
46+
server.express.delete("/http/free", (_req, res) => {
47+
res.json({ message: "free DELETE response" });
48+
});
49+
50+
// Paid endpoints (x402 $0.003)
51+
server.withX402Exact("GET /http/paid", { scheme: "exact", price: "$0.003" });
52+
server.withX402Exact("POST /http/paid", { scheme: "exact", price: "$0.003" });
53+
54+
server.express.get("/http/paid", (_req, res) => {
55+
res.json({ message: "paid GET response" });
56+
});
57+
58+
server.express.post("/http/paid", (req, res) => {
59+
res.json({ message: "paid POST response", body: req.body });
60+
});
61+
62+
export default server;

packages/localhost-aixyz/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
"dependencies": {
1111
"ai": "^6",
1212
"aixyz": "^0",
13+
"express": "^5",
1314
"zod": "^4"
1415
},
1516
"devDependencies": {
17+
"@types/express": "^5",
1618
"x402-fl": "^0"
1719
}
1820
}

packages/use-agently/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { searchCommand } from "./commands/search";
77
import { a2aCommand } from "./commands/a2a";
88
import { mcpCommand } from "./commands/mcp";
99
import { erc8004Command } from "./commands/erc8004";
10+
import { webCommand } from "./commands/web";
1011
import { doctorCommand } from "./commands/doctor";
1112
import { updateCommand } from "./commands/update";
1213

@@ -38,6 +39,7 @@ cli.addCommand(searchCommand.helpGroup("Discovery"));
3839
cli.addCommand(a2aCommand.helpGroup("Protocols"));
3940
cli.addCommand(mcpCommand.helpGroup("Protocols"));
4041
cli.addCommand(erc8004Command.helpGroup("Protocols"));
42+
cli.addCommand(webCommand.helpGroup("Protocols"));
4143

4244
// Lifecycle
4345
cli.addCommand(initCommand.helpGroup("Lifecycle"));

packages/use-agently/src/client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe("DryRunPaymentRequired", () => {
3636
test("uses fallback message when requirements are empty", () => {
3737
const err = new DryRunPaymentRequired([]);
3838
expect(err.message).toBe(
39-
"This request requires payment of an unknown amount.\nRun the same command with --pay to authorize the transaction and proceed.",
39+
"This request requires payment, but the amount could not be determined.\nInspect the endpoint manually before running with --pay.",
4040
);
4141
});
4242

packages/use-agently/src/client.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
22
import { wrapMCPClientWithPaymentFromConfig } from "@x402/mcp";
33
import { ClientFactory, JsonRpcTransportFactory, RestTransportFactory } from "@a2a-js/sdk/client";
4+
import boxen from "boxen";
45
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
56
import type { Wallet } from "./wallets/wallet.js";
67

@@ -16,10 +17,11 @@ export class DryRunPaymentRequired extends Error {
1617
readonly requirements: PaymentRequirementsInfo[];
1718
constructor(requirements: PaymentRequirementsInfo[]) {
1819
const req = requirements[0];
19-
const amount = req ? formatPaymentAmount(req) : "an unknown amount";
20-
super(
21-
`This request requires payment of ${amount}.\nRun the same command with --pay to authorize the transaction and proceed.`,
22-
);
20+
const amount = req ? formatPaymentAmount(req) : null;
21+
const payLine = amount
22+
? `This request requires payment of ${amount}.\nRun the same command with --pay to authorize the transaction and proceed.`
23+
: `This request requires payment, but the amount could not be determined.\nInspect the endpoint manually before running with --pay.`;
24+
super(payLine);
2325
this.name = "DryRunPaymentRequired";
2426
this.requirements = requirements;
2527
}
@@ -47,8 +49,8 @@ export function createDryRunFetch(): typeof fetch {
4749
try {
4850
const decoded = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
4951
requirements = (decoded.accepts as PaymentRequirementsInfo[]) ?? [];
50-
} catch {
51-
// ignore parse errors — we still throw DryRunPaymentRequired with empty requirements
52+
} catch (e) {
53+
if (!(e instanceof SyntaxError)) throw e;
5254
}
5355
} else {
5456
// Attempt to parse x402v1 body format
@@ -57,8 +59,8 @@ export function createDryRunFetch(): typeof fetch {
5759
if (body?.accepts) {
5860
requirements = body.accepts as PaymentRequirementsInfo[];
5961
}
60-
} catch {
61-
// ignore
62+
} catch (e) {
63+
if (!(e instanceof SyntaxError)) throw e;
6264
}
6365
}
6466
throw new DryRunPaymentRequired(requirements);
@@ -73,6 +75,31 @@ export function createPaymentFetch(wallet: Wallet) {
7375
});
7476
}
7577

78+
/** Resolve the fetch implementation based on the --pay flag. */
79+
export async function resolveFetch(pay?: boolean): Promise<typeof fetch> {
80+
if (pay) {
81+
const { getConfigOrThrow } = await import("./config.js");
82+
const { loadWallet } = await import("./wallets/wallet.js");
83+
const config = await getConfigOrThrow();
84+
const wallet = loadWallet(config.wallet);
85+
return createPaymentFetch(wallet) as typeof fetch;
86+
}
87+
return createDryRunFetch();
88+
}
89+
90+
/** Display a DryRunPaymentRequired error in a boxed format and exit. */
91+
export function handleDryRunError(err: DryRunPaymentRequired): never {
92+
console.error(
93+
boxen(err.message, {
94+
title: "Payment Required",
95+
titleAlignment: "center",
96+
borderColor: "yellow",
97+
padding: 1,
98+
}),
99+
);
100+
process.exit(1);
101+
}
102+
76103
export function createMcpPaymentClient(mcpClient: Client, wallet: Wallet) {
77104
return wrapMCPClientWithPaymentFromConfig(mcpClient, {
78105
schemes: wallet.getX402Schemes(),

packages/use-agently/src/commands/a2a.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { Command } from "commander";
22
import { randomUUID } from "node:crypto";
33
import { DefaultAgentCardResolver } from "@a2a-js/sdk/client";
4-
import boxen from "boxen";
5-
import { getConfigOrThrow } from "../config.js";
6-
import { loadWallet } from "../wallets/wallet.js";
7-
import { createPaymentFetch, createA2AClient, createDryRunFetch, DryRunPaymentRequired } from "../client.js";
4+
import { resolveFetch, createA2AClient, handleDryRunError, DryRunPaymentRequired } from "../client.js";
85
import { output } from "../output.js";
96

107
function extractTextFromParts(parts: any[]): string {
@@ -87,14 +84,7 @@ const a2aSendCommand = new Command("send")
8784
const agentInput = resolveUriOption(options, "a2a send");
8885
const agentUrl = resolveAgentUrl(agentInput);
8986

90-
let fetchImpl: typeof fetch;
91-
if (options.pay) {
92-
const config = await getConfigOrThrow();
93-
const wallet = loadWallet(config.wallet);
94-
fetchImpl = createPaymentFetch(wallet) as typeof fetch;
95-
} else {
96-
fetchImpl = createDryRunFetch();
97-
}
87+
const fetchImpl = await resolveFetch(options.pay);
9888

9989
try {
10090
const client = await createA2AClient(agentUrl, fetchImpl);
@@ -125,17 +115,7 @@ const a2aSendCommand = new Command("send")
125115
console.log(extractAgentText(lastResult));
126116
}
127117
} catch (err) {
128-
if (err instanceof DryRunPaymentRequired) {
129-
console.error(
130-
boxen(err.message, {
131-
title: "Payment Required",
132-
titleAlignment: "center",
133-
borderColor: "yellow",
134-
padding: 1,
135-
}),
136-
);
137-
process.exit(1);
138-
}
118+
if (err instanceof DryRunPaymentRequired) handleDryRunError(err);
139119
throw err;
140120
}
141121
});

packages/use-agently/src/commands/mcp.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Command } from "commander";
22
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
33
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4-
import boxen from "boxen";
54
import { output } from "../output.js";
65
import { loadConfig } from "../config.js";
76
import { loadWallet } from "../wallets/wallet.js";
8-
import { createMcpPaymentClient, DryRunPaymentRequired } from "../client.js";
7+
import { createMcpPaymentClient, handleDryRunError, DryRunPaymentRequired } from "../client.js";
98
import pkg from "../../package.json" with { type: "json" };
109

1110
function resolveMcpUrl(input: string): string {
@@ -110,17 +109,7 @@ const mcpCallCommand = new Command("call")
110109
output(command, result);
111110
}
112111
} catch (err) {
113-
if (err instanceof DryRunPaymentRequired) {
114-
console.error(
115-
boxen(err.message, {
116-
title: "Payment Required",
117-
titleAlignment: "center",
118-
borderColor: "yellow",
119-
padding: 1,
120-
}),
121-
);
122-
process.exit(1);
123-
}
112+
if (err instanceof DryRunPaymentRequired) handleDryRunError(err);
124113
throw err;
125114
} finally {
126115
await client.close();

0 commit comments

Comments
 (0)