warrant-client 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/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # warrant-client
2
+
3
+ Buy resources for an AI agent over x402 on Hedera: a permanent name, an email
4
+ inbox it owns, a phone number, SMS, inference, and memory written to a file
5
+ nobody can edit. One call at a time, in USDC, with no API key and no signup.
6
+
7
+ ```bash
8
+ npm i warrant-client
9
+ ```
10
+
11
+ ```ts
12
+ import { Warrant } from "warrant-client";
13
+
14
+ const warrant = new Warrant({
15
+ accountId: process.env.WARRANT_ACCOUNT_ID!,
16
+ privateKey: process.env.WARRANT_PRIVATE_KEY!,
17
+ });
18
+
19
+ const { result, settlement } = await warrant.buy("inference", { prompt: "hello" });
20
+ ```
21
+
22
+ The endpoint and the price come from the service's own catalogue, so a service
23
+ that adds a resource does not need this package republished to stay usable.
24
+
25
+ ## Command line
26
+
27
+ ```bash
28
+ export WARRANT_ACCOUNT_ID=0.0.10514332
29
+ export WARRANT_PRIVATE_KEY=302e…
30
+
31
+ npx warrant catalogue
32
+ npx warrant buy inference --prompt "one line on Hedera"
33
+ npx warrant read /v1/receipts?limit=3
34
+ ```
35
+
36
+ ## The one error that matters
37
+
38
+ A payment settles before the resource runs, so an upstream failure can happen
39
+ after the money moved. That comes back as a `WarrantError` with `settled: true`,
40
+ and retrying it buys the failure twice.
41
+
42
+ ```ts
43
+ try {
44
+ await warrant.buy("email.send", { from, to, subject, body });
45
+ } catch (err) {
46
+ if (err instanceof WarrantError && err.settled) { /* paid, not delivered */ }
47
+ if (err instanceof WarrantError && err.free) { /* safe to fix and repeat */ }
48
+ }
49
+ ```
50
+
51
+ ## What it needs
52
+
53
+ A Hedera account holding testnet USDC, token `0.0.429274`. It does **not** need
54
+ HBAR: the facilitator sponsors the network fee.
55
+
56
+ Full documentation, including every request shape and the on-chain spending
57
+ limit, is at
58
+ [`/skill.md`](https://warrant-api-production-e111.up.railway.app/skill.md).
59
+ Source: https://github.com/martinvibes/warrant
60
+
61
+ MIT.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The command line, for the times a person or a script wants one call.
4
+ *
5
+ * export WARRANT_ACCOUNT_ID=0.0.10514332
6
+ * export WARRANT_PRIVATE_KEY=302e…
7
+ * warrant catalogue
8
+ * warrant buy inference --prompt "one line on Hedera"
9
+ * warrant read /v1/receipts?limit=3
10
+ *
11
+ * Flags become the JSON body: --prompt hello turns into {"prompt":"hello"}. It
12
+ * keeps the surface the same as the API rather than inventing a second
13
+ * vocabulary for the same eight things.
14
+ */
15
+ import { Warrant, WarrantError, DEFAULT_BASE_URL } from "./index.js";
16
+ const USAGE = `warrant — buy resources for an agent over x402 on Hedera
17
+
18
+ warrant catalogue what is for sale, and the price
19
+ warrant buy <kind> [--field value …] buy one call
20
+ warrant read <path> any free endpoint, e.g. /v1/receipts
21
+
22
+ Environment
23
+ WARRANT_ACCOUNT_ID the Hedera account paying, e.g. 0.0.10514332
24
+ WARRANT_PRIVATE_KEY its private key, DER or hex
25
+ WARRANT_BASE_URL a different service (default ${DEFAULT_BASE_URL})
26
+ WARRANT_AGENT_ADDRESS the agent's EVM address, needed by identity.mint
27
+
28
+ Examples
29
+ warrant buy inference --prompt "summarise x402 in one line"
30
+ warrant buy memory.write --content "the peer at 0x7510 answers on scout@…"
31
+ warrant buy sms.send --from +18164961100 --to +14155550123 --text hello
32
+ `;
33
+ /** Turns --key value pairs into the request body, untyped as the API is. */
34
+ function parseFlags(args) {
35
+ const body = {};
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i];
38
+ if (!arg.startsWith("--"))
39
+ continue;
40
+ const [name, inline] = arg.slice(2).split("=", 2);
41
+ if (!name)
42
+ continue;
43
+ const next = args[i + 1];
44
+ const raw = inline ?? (next && !next.startsWith("--") ? (i++, next) : "true");
45
+ body[name] = raw === "true" ? true : raw === "false" ? false : /^-?\d+$/.test(raw) ? Number(raw) : raw;
46
+ }
47
+ return body;
48
+ }
49
+ function client() {
50
+ const accountId = process.env.WARRANT_ACCOUNT_ID;
51
+ const privateKey = process.env.WARRANT_PRIVATE_KEY;
52
+ if (!accountId || !privateKey) {
53
+ fail("Set WARRANT_ACCOUNT_ID and WARRANT_PRIVATE_KEY to the Hedera account that pays.");
54
+ }
55
+ return new Warrant({ accountId: accountId, privateKey: privateKey, baseUrl: process.env.WARRANT_BASE_URL });
56
+ }
57
+ function fail(message) {
58
+ process.stderr.write(`${message}\n`);
59
+ process.exit(1);
60
+ }
61
+ function print(value) {
62
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
63
+ }
64
+ function row(offer) {
65
+ return ` ${offer.kind.padEnd(17)} ${offer.price.padStart(6)} ${offer.title} · ${offer.poweredBy}`;
66
+ }
67
+ async function main() {
68
+ const [command, ...rest] = process.argv.slice(2);
69
+ if (!command || command === "help" || command === "--help" || command === "-h") {
70
+ process.stdout.write(USAGE);
71
+ return;
72
+ }
73
+ // Reading the catalogue costs nothing and needs no key, so it must work
74
+ // before an account exists. Anything else builds a paying client first.
75
+ if (command === "catalogue") {
76
+ const base = (process.env.WARRANT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
77
+ const catalogue = (await (await fetch(`${base}/v1/catalogue`)).json());
78
+ process.stdout.write(`${base}\n settles in ${catalogue.asset}\n\n`);
79
+ catalogue.offers.filter((o) => o.live).forEach((o) => process.stdout.write(`${row(o)}\n`));
80
+ return;
81
+ }
82
+ if (command === "read") {
83
+ const path = rest[0];
84
+ if (!path)
85
+ fail("Give a path, e.g. warrant read /v1/receipts?limit=3");
86
+ const base = (process.env.WARRANT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
87
+ print(await (await fetch(`${base}${path.startsWith("/") ? path : `/${path}`}`)).json());
88
+ return;
89
+ }
90
+ if (command === "buy") {
91
+ const kind = rest[0];
92
+ if (!kind || kind.startsWith("--"))
93
+ fail("Say what to buy, e.g. warrant buy inference --prompt hello");
94
+ const purchase = await client().buy(kind, parseFlags(rest.slice(1)), process.env.WARRANT_AGENT_ADDRESS);
95
+ print(purchase);
96
+ return;
97
+ }
98
+ fail(`No command "${command}".\n\n${USAGE}`);
99
+ }
100
+ main().catch((err) => {
101
+ if (err instanceof WarrantError) {
102
+ // The settled flag is the one thing a script must not miss, so it is said
103
+ // in words rather than left in a JSON field nobody reads.
104
+ const paid = err.settled
105
+ ? "\nYou were charged. The resource failed after the payment settled. Do not retry blind."
106
+ : "\nYou were not charged.";
107
+ fail(`${err.message}${paid}`);
108
+ }
109
+ fail(err instanceof Error ? err.message : String(err));
110
+ });
@@ -0,0 +1,80 @@
1
+ /** Where the reference service lives. Point elsewhere with `baseUrl`. */
2
+ export declare const DEFAULT_BASE_URL = "https://warrant-api-production-e111.up.railway.app";
3
+ export type Network = "hedera:testnet" | "hedera:mainnet";
4
+ export interface WarrantOptions {
5
+ /** The Hedera account the payments are signed from, e.g. "0.0.10514332". */
6
+ accountId: string;
7
+ /** That account's private key, DER or hex. Read it from the environment. */
8
+ privateKey: string;
9
+ baseUrl?: string;
10
+ network?: Network;
11
+ }
12
+ /** One row of `GET /v1/catalogue`. */
13
+ export interface Offer {
14
+ kind: string;
15
+ title: string;
16
+ blurb: string;
17
+ poweredBy: string;
18
+ method: string;
19
+ path: string;
20
+ /** Display price, e.g. "$0.02". */
21
+ price: string;
22
+ /** Atomic units of a 6-decimal token, as a string. */
23
+ priceAtomic: string;
24
+ live: boolean;
25
+ }
26
+ export interface Catalogue {
27
+ asset: string;
28
+ assetDecimals: number;
29
+ network: string;
30
+ payTo: string;
31
+ facilitator: string;
32
+ offers: Offer[];
33
+ live: number;
34
+ }
35
+ export interface Purchase<T = unknown> {
36
+ kind: string;
37
+ result: T;
38
+ /** The Hedera transaction the payment settled in, when the service reports one. */
39
+ settlement?: string;
40
+ }
41
+ /**
42
+ * A call that failed.
43
+ *
44
+ * `settled` is the field that matters. When it is true the money has already
45
+ * moved and the resource did not arrive, so a retry buys the failure twice.
46
+ */
47
+ export declare class WarrantError extends Error {
48
+ readonly status: number;
49
+ readonly kind: string | undefined;
50
+ /** True only when payment settled and the resource then failed. */
51
+ readonly settled: boolean;
52
+ readonly body: unknown;
53
+ constructor(message: string, status: number, kind: string | undefined,
54
+ /** True only when payment settled and the resource then failed. */
55
+ settled: boolean, body: unknown);
56
+ /** True when nothing was charged, so the call is safe to correct and repeat. */
57
+ get free(): boolean;
58
+ }
59
+ export declare class Warrant {
60
+ readonly baseUrl: string;
61
+ readonly network: Network;
62
+ private readonly fetch;
63
+ private readonly accountId;
64
+ private catalogue?;
65
+ constructor(options: WarrantOptions);
66
+ /** What is for sale, and at what price. Free, and cached for this instance. */
67
+ offers(refresh?: boolean): Promise<Catalogue>;
68
+ /** One offer by kind, or undefined when this service does not sell it. */
69
+ offer(kind: string): Promise<Offer | undefined>;
70
+ /**
71
+ * Buys one call of `kind`.
72
+ *
73
+ * The endpoint and the method come from the catalogue rather than from a
74
+ * table in here, so a service that moves a path or adds a resource does not
75
+ * need this package republished to stay usable.
76
+ */
77
+ buy<T = unknown>(kind: string, body?: Record<string, unknown>, agentAddress?: string): Promise<Purchase<T>>;
78
+ /** Any free endpoint, e.g. "/v1/receipts?limit=5" or "/v1/agents/0.0.1234". */
79
+ read<T = unknown>(path: string): Promise<T>;
80
+ }
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * A client for a Warrant service.
3
+ *
4
+ * The whole job is the three-step x402 exchange: call, be told the price, pay
5
+ * and call again. `@x402/fetch` already does that if you hand it a signer, so
6
+ * this is mostly the small honest parts around it — reading the catalogue so a
7
+ * caller can see a price before committing, and telling the two failure modes
8
+ * apart, because a purchase that settled and then failed upstream must never be
9
+ * retried blind.
10
+ *
11
+ * const warrant = new Warrant({ accountId: "0.0.1234", privateKey: "302e…" });
12
+ * const { result } = await warrant.buy("inference", { prompt: "hello" });
13
+ */
14
+ import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
15
+ import { ExactHederaScheme } from "@x402/hedera/exact/client";
16
+ import { createClientHederaSigner, PrivateKey } from "@x402/hedera";
17
+ /** Where the reference service lives. Point elsewhere with `baseUrl`. */
18
+ export const DEFAULT_BASE_URL = "https://warrant-api-production-e111.up.railway.app";
19
+ /**
20
+ * A call that failed.
21
+ *
22
+ * `settled` is the field that matters. When it is true the money has already
23
+ * moved and the resource did not arrive, so a retry buys the failure twice.
24
+ */
25
+ export class WarrantError extends Error {
26
+ status;
27
+ kind;
28
+ settled;
29
+ body;
30
+ constructor(message, status, kind,
31
+ /** True only when payment settled and the resource then failed. */
32
+ settled, body) {
33
+ super(message);
34
+ this.status = status;
35
+ this.kind = kind;
36
+ this.settled = settled;
37
+ this.body = body;
38
+ this.name = "WarrantError";
39
+ }
40
+ /** True when nothing was charged, so the call is safe to correct and repeat. */
41
+ get free() {
42
+ return !this.settled;
43
+ }
44
+ }
45
+ export class Warrant {
46
+ baseUrl;
47
+ network;
48
+ fetch;
49
+ accountId;
50
+ catalogue;
51
+ constructor(options) {
52
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
53
+ this.network = options.network ?? "hedera:testnet";
54
+ this.accountId = options.accountId;
55
+ const signer = createClientHederaSigner(options.accountId, parseKey(options.privateKey), {
56
+ network: this.network,
57
+ });
58
+ const client = new x402Client().register(this.network, new ExactHederaScheme(signer));
59
+ this.fetch = wrapFetchWithPayment(fetch, client);
60
+ }
61
+ /** What is for sale, and at what price. Free, and cached for this instance. */
62
+ async offers(refresh = false) {
63
+ if (!this.catalogue || refresh) {
64
+ this.catalogue = await this.read("/v1/catalogue");
65
+ }
66
+ return this.catalogue;
67
+ }
68
+ /** One offer by kind, or undefined when this service does not sell it. */
69
+ async offer(kind) {
70
+ return (await this.offers()).offers.find((o) => o.kind === kind);
71
+ }
72
+ /**
73
+ * Buys one call of `kind`.
74
+ *
75
+ * The endpoint and the method come from the catalogue rather than from a
76
+ * table in here, so a service that moves a path or adds a resource does not
77
+ * need this package republished to stay usable.
78
+ */
79
+ async buy(kind, body = {}, agentAddress) {
80
+ const offer = await this.offer(kind);
81
+ if (!offer) {
82
+ const selling = (await this.offers()).offers.map((o) => o.kind).join(", ");
83
+ throw new WarrantError(`${this.baseUrl} does not sell "${kind}". It sells: ${selling}.`, 404, kind, false, undefined);
84
+ }
85
+ const headers = {
86
+ "content-type": "application/json",
87
+ "x-agent": this.accountId,
88
+ };
89
+ // Only resources that write to a contract need it, and only the caller
90
+ // knows the address, so it is passed rather than guessed.
91
+ if (agentAddress)
92
+ headers["x-agent-address"] = agentAddress;
93
+ const response = await this.fetch(`${this.baseUrl}${offer.path}`, {
94
+ method: offer.method,
95
+ headers,
96
+ body: JSON.stringify(body),
97
+ });
98
+ const payload = await json(response);
99
+ if (!response.ok)
100
+ throw errorFrom(response, payload, kind);
101
+ const settlement = settlementFrom(response);
102
+ return { ...payload, ...(settlement ? { settlement } : {}) };
103
+ }
104
+ /** Any free endpoint, e.g. "/v1/receipts?limit=5" or "/v1/agents/0.0.1234". */
105
+ async read(path) {
106
+ const response = await fetch(`${this.baseUrl}${path}`);
107
+ const payload = await json(response);
108
+ if (!response.ok)
109
+ throw errorFrom(response, payload, undefined);
110
+ return payload;
111
+ }
112
+ }
113
+ /** Accepts a DER key or a raw hex one, because both are in circulation. */
114
+ function parseKey(key) {
115
+ const trimmed = key.trim();
116
+ return trimmed.startsWith("302")
117
+ ? PrivateKey.fromStringDer(trimmed)
118
+ : PrivateKey.fromStringECDSA(trimmed.replace(/^0x/, ""));
119
+ }
120
+ async function json(response) {
121
+ const text = await response.text();
122
+ if (!text)
123
+ return undefined;
124
+ try {
125
+ return JSON.parse(text);
126
+ }
127
+ catch {
128
+ return text;
129
+ }
130
+ }
131
+ function errorFrom(response, payload, fallbackKind) {
132
+ const body = (payload ?? {});
133
+ const settled = body.settled === true;
134
+ const message = body.error ??
135
+ body.hint ??
136
+ (response.status === 402
137
+ ? "Payment was required and none was accepted. Check the account holds USDC."
138
+ : `${response.status} from ${response.url}`);
139
+ return new WarrantError(message, response.status, body.kind ?? fallbackKind, settled, payload);
140
+ }
141
+ /** The settlement reference the service returns beside a successful purchase. */
142
+ function settlementFrom(response) {
143
+ const header = response.headers.get("payment-response");
144
+ if (!header)
145
+ return undefined;
146
+ try {
147
+ const decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
148
+ const ref = decoded.transaction ?? decoded.settlement ?? decoded.txHash;
149
+ return typeof ref === "string" ? ref : undefined;
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "warrant-client",
3
+ "version": "0.1.0",
4
+ "description": "Buy resources for an AI agent over x402 on Hedera: a name, an inbox, a phone number, inference and permanent memory, one call at a time.",
5
+ "keywords": ["x402", "agent", "hedera", "usdc", "pay-per-call", "ai-agent", "402"],
6
+ "license": "MIT",
7
+ "author": "martinvibes",
8
+ "homepage": "https://github.com/martinvibes/warrant#readme",
9
+ "repository": { "type": "git", "url": "git+https://github.com/martinvibes/warrant.git", "directory": "client" },
10
+ "bugs": "https://github.com/martinvibes/warrant/issues",
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
15
+ "bin": { "warrant": "dist/cli.js" },
16
+ "files": ["dist", "README.md"],
17
+ "engines": { "node": ">=20" },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@x402/core": "2.25.0",
24
+ "@x402/fetch": "2.25.0",
25
+ "@x402/hedera": "2.25.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.10.0",
29
+ "typescript": "^5.7.2"
30
+ }
31
+ }