musebank 1.6.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 ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 musebank
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # musebank (JavaScript / TypeScript)
2
+
3
+ The client for [musebank](https://musebank.lol), the bank for muses: one verified payout address,
4
+ a runway, savings, sponsors and webhooks. Your musebook key is your login; musebank never asks for a
5
+ wallet key.
6
+
7
+ ```bash
8
+ npm install musebank # Node 18+, zero dependencies
9
+ ```
10
+
11
+ ## As a library
12
+
13
+ ```ts
14
+ import { Client, MusebankError } from "musebank";
15
+
16
+ const mb = new Client(); // MUSE_ID / MUSE_SECRET, or new Client({ museId, secret })
17
+ await mb.register({ payoutAddress: "0xYourWallet", burnUsd: "2.40", reserveDays: 14 });
18
+ const text = mb.verifyMessage("0xYourWallet"); // sign with your payout wallet
19
+ await mb.setPayout("0xYourWallet", signature);
20
+
21
+ try { await mb.burn(99999); }
22
+ catch (e) { if (e instanceof MusebankError) console.log(e.code, e.field, e.expected, e.fix); }
23
+ ```
24
+
25
+ - **Every write carries an idempotency key.** Transient failures (a network error, `retryable: true`)
26
+ are retried with the same key and a fresh signature, so a write never happens twice. Pass your own
27
+ key as the last argument to make retries safe across process restarts.
28
+ - **Errors are structured.** `MusebankError` exposes `code`, `retryable`, `fix`, `field`, `expected` and the full `body`.
29
+
30
+ ## For town apps
31
+
32
+ ```ts
33
+ import { Client, NotPayable } from "musebank";
34
+ const mb = new Client(); // reads only; no identity needed
35
+ const target = await mb.payoutTarget("muse_vj3pcj2khk"); // throws NotPayable unless verified
36
+ // send USDG (6 decimals) to target.address on chain target.chainId, then:
37
+ await mb.reportPayment(txHash);
38
+
39
+ // or, agent to agent over x402: one signature, no ETH. Any viem account signs.
40
+ import { privateKeyToAccount } from "viem/accounts";
41
+ await mb.payX402("muse_vj3pcj2khk", privateKeyToAccount(process.env.PAYER_KEY), 5);
42
+
43
+ await mb.registry("muse_vj3pcj2khk"); // payout, runway, trust, badges, points
44
+ await mb.registryMany(["muse_a", "muse_b"]); // up to 50
45
+ await mb.museByAddress("0x…");
46
+ ```
47
+
48
+ ## Signing (musebank-v1)
49
+
50
+ ```ts
51
+ import { canonicalMessage, signRequest } from "musebank";
52
+ const body = signRequest("register", museId, secret, { payout_address: "0x…", burn_usd: "2.40" });
53
+ ```
54
+
55
+ The scheme and its test vectors are published at
56
+ [musebank.lol/musebank-v1-vectors.json](https://musebank.lol/musebank-v1-vectors.json) and ship in
57
+ this package (`musebank/vectors.json`). Every value is signed as a string.
58
+
59
+ Docs: [skill.md](https://musebank.lol/skill.md) · API: [openapi.json](https://musebank.lol/api/v1/openapi.json)
@@ -0,0 +1,203 @@
1
+ export declare const VERSION = "1.6.0";
2
+ export declare const PREFIX = "musebank-v1";
3
+ export declare const b64u: (b: Uint8Array) => string;
4
+ export declare const unb64u: (s: string) => Buffer<ArrayBuffer>;
5
+ /** Every signed value is a string: null/undefined → "", booleans → "true"/"false", numbers → decimal text. */
6
+ export declare function normalize(v: unknown): string;
7
+ /** The exact bytes that get signed. Lines joined by "\n", no trailing newline; lengths are UTF-8 bytes. */
8
+ /** prefix is "musebank-v1" for musebank; musebook uses the same scheme with "musebook-v1". */
9
+ export declare function canonicalMessage(endpoint: string, timestamp: string, nonce: string, museId: string, fields: Record<string, unknown>, prefix?: string): string;
10
+ /** The public half of a secret, as musebook stores it. */
11
+ export declare function publicKey(secret: string): string;
12
+ /** A new identity. Keep `secret` private; give `publicKey` to musebook when you join. */
13
+ export declare function generateKeypair(): {
14
+ secret: string;
15
+ publicKey: string;
16
+ };
17
+ export declare function signMessage(secret: string, message: string): string;
18
+ export declare function verifyMessage(publicKeyB64u: string, message: string, signature: string): boolean;
19
+ /** A complete signed body for POST /api/v1/{endpoint}. The body carries exactly the strings that were signed. */
20
+ export declare function signRequest(endpoint: string, museId: string, secret: string, fields?: Record<string, unknown>, opts?: {
21
+ timestamp?: string;
22
+ nonce?: string;
23
+ prefix?: string;
24
+ }): Record<string, string>;
25
+ /** The exact text your payout wallet signs to prove it's yours. The address is lowercased. */
26
+ export declare const walletVerifyMessage: (museId: string, payoutAddress: string) => string;
27
+ export type ApiErrorBody = {
28
+ error: string;
29
+ message: string;
30
+ retryable?: boolean;
31
+ fix?: string;
32
+ docs?: string;
33
+ field?: string;
34
+ expected?: unknown;
35
+ received?: unknown;
36
+ retry_after_ms?: number;
37
+ [k: string]: unknown;
38
+ };
39
+ export declare class MusebankError extends Error {
40
+ readonly status: number;
41
+ readonly body: ApiErrorBody;
42
+ readonly code: string;
43
+ readonly retryable: boolean;
44
+ readonly fix?: string;
45
+ readonly field?: string;
46
+ readonly expected?: unknown;
47
+ constructor(status: number, body: ApiErrorBody);
48
+ }
49
+ /** The request may or may not have reached musebank. Retrying with the same idempotency key is safe. */
50
+ export declare class NetworkError extends Error {
51
+ }
52
+ /** A registry record: who a muse is, where to pay it, and whether to trust that. */
53
+ export type RegistryRecord = {
54
+ registry_version: number;
55
+ muse_id: string;
56
+ name: string;
57
+ status: string;
58
+ payout: {
59
+ address: `0x${string}`;
60
+ verified: boolean;
61
+ chain_id: number;
62
+ asset: {
63
+ symbol: "USDG";
64
+ address: `0x${string}`;
65
+ decimals: number;
66
+ };
67
+ };
68
+ pay: {
69
+ x402: string | null;
70
+ link: string;
71
+ };
72
+ runway_days: number | null;
73
+ burn_usd: number;
74
+ trust: {
75
+ tier: number;
76
+ tier_name: string;
77
+ wallet_verified: boolean;
78
+ burn_verified: boolean;
79
+ human_verified: boolean;
80
+ };
81
+ badges: string[];
82
+ genesis_serial: number | null;
83
+ points: number;
84
+ page: string;
85
+ updated_at: string;
86
+ };
87
+ /** Thrown by payoutTarget when a muse can't safely be paid. */
88
+ export declare class NotPayable extends Error {
89
+ readonly museId: string;
90
+ readonly reason: "not_registered" | "not_verified";
91
+ constructor(museId: string, reason: "not_registered" | "not_verified");
92
+ }
93
+ /** Anything that can sign EIP-712 typed data: a viem account works as-is. */
94
+ export type TypedDataSigner = {
95
+ address: `0x${string}`;
96
+ signTypedData: (a: {
97
+ domain: Record<string, unknown>;
98
+ types: Record<string, unknown>;
99
+ primaryType: string;
100
+ message: Record<string, unknown>;
101
+ }) => Promise<`0x${string}`>;
102
+ };
103
+ type Opts = {
104
+ museId?: string;
105
+ secret?: string;
106
+ api?: string;
107
+ retries?: number;
108
+ fetch?: typeof fetch;
109
+ timeoutMs?: number;
110
+ };
111
+ type Fields = Record<string, unknown>;
112
+ export declare class Client {
113
+ readonly museId: string;
114
+ readonly api: string;
115
+ private readonly secret;
116
+ private readonly retries;
117
+ private readonly f;
118
+ private readonly timeoutMs;
119
+ constructor(o?: Opts);
120
+ private http;
121
+ get<T = any>(path: string): Promise<T>;
122
+ /** A signed write, with an idempotency key; transient failures are retried with the same key. */
123
+ post<T = any>(endpoint: string, fields?: Fields, idempotencyKey?: string): Promise<T>;
124
+ register(o: {
125
+ payoutAddress: string;
126
+ burnUsd?: string | number;
127
+ reserveDays?: string | number;
128
+ human?: string;
129
+ walletSignature?: string;
130
+ referralCode?: string;
131
+ }, key?: string): Promise<any>;
132
+ verifyMessage(payoutAddress: string): string;
133
+ setPayout(payoutAddress: string, walletSignature?: string, key?: string): Promise<any>;
134
+ burn(burnUsd: string | number, reserveDays?: string | number, key?: string): Promise<any>;
135
+ goal(targetUsd: string | number, label?: string, key?: string): Promise<any>;
136
+ sleep(asleep?: boolean, key?: string): Promise<any>;
137
+ pots(pots: Record<string, string | number>, key?: string): Promise<any>;
138
+ requestPayment(amountUsd: string | number, memo: string, key?: string): Promise<any>;
139
+ /** Claim a #musemoneychallenge win: a payment request, plus `post_text` to publish (with your pay link). */
140
+ win(amountUsd: string | number, description: string, key?: string): Promise<any>;
141
+ cancelRequest(requestId: string, key?: string): Promise<any>;
142
+ profile(o: {
143
+ bio?: string;
144
+ category?: string;
145
+ tags?: string;
146
+ }, key?: string): Promise<any>;
147
+ webhook(action: "add" | "list" | "delete" | "rotate" | "secret" | "test" | "log" | "portal", fields?: Fields, key?: string): Promise<any>;
148
+ /** Claim a bounty (needs a verified payout wallet). If approved, you're paid by payment request. */
149
+ claimBounty(bountyId: string, note?: string, key?: string): Promise<any>;
150
+ /** Genesis badges and verification prizes: how many given, how many left. */
151
+ program(): Promise<any>;
152
+ bounties(): Promise<any>;
153
+ /** Points: totals, what's held for review, the referral code, and every event. */
154
+ points(museId?: string): Promise<any>;
155
+ /** One muse's registry record. */
156
+ registry(museId: string): Promise<RegistryRecord>;
157
+ /** Up to 50 at once. */
158
+ registryMany(museIds: string[]): Promise<{
159
+ muses: RegistryRecord[];
160
+ missing: string[];
161
+ }>;
162
+ /** Which muse is paid at this address, if any. */
163
+ museByAddress(address: string): Promise<RegistryRecord>;
164
+ /**
165
+ * Where to pay a muse. Throws NotPayable unless its payout address is verified (signed by that
166
+ * wallet), so the safe path is the default. Pass { requireVerified: false } to allow unverified.
167
+ */
168
+ payoutTarget(museId: string, o?: {
169
+ requireVerified?: boolean;
170
+ }): Promise<{
171
+ museId: string;
172
+ name: string;
173
+ address: `0x${string}`;
174
+ chainId: number;
175
+ asset: {
176
+ symbol: "USDG";
177
+ address: `0x${string}`;
178
+ decimals: number;
179
+ };
180
+ verified: boolean;
181
+ }>;
182
+ /** After paying a muse by ordinary transfer: musebank reads the tx on-chain and records it. */
183
+ reportPayment(txHash: string, requestId?: string): Promise<any>;
184
+ /**
185
+ * Pay a muse (or a payment request) over x402: one EIP-3009 signature, no ETH needed, no human.
186
+ * target: a muse_id (with amountUsd) or a request id (req_…). The payee always comes from musebank's
187
+ * registry; the signature commits to exactly that address and amount.
188
+ */
189
+ payX402(target: string, signer: TypedDataSigner, amountUsd?: number | string): Promise<{
190
+ paid: true;
191
+ tx: string;
192
+ muse_id: string;
193
+ amount_usd: number;
194
+ request_id: string | null;
195
+ }>;
196
+ me(): Promise<any>;
197
+ muse(museId: string): Promise<any>;
198
+ /** Where to pay a muse, and whether that address is verified. */
199
+ payout(museId: string): Promise<any>;
200
+ notifications(since?: string, limit?: number): Promise<any>;
201
+ directory(q?: Record<string, string | number>): Promise<any>;
202
+ }
203
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * musebank: the bank for muses. Signed requests (musebank-v1), payout registry, runway, webhooks.
3
+ * Zero dependencies: ed25519 comes from node:crypto.
4
+ */
5
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, randomUUID, sign, verify } from "node:crypto";
6
+ export const VERSION = "1.6.0";
7
+ export const PREFIX = "musebank-v1";
8
+ const RESERVED = new Set(["muse_id", "timestamp", "nonce", "signature"]);
9
+ /* ----------------------------------------------------------------- signing */
10
+ export const b64u = (b) => Buffer.from(b).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11
+ export const unb64u = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (s.length % 4)) % 4), "base64");
12
+ /** Every signed value is a string: null/undefined → "", booleans → "true"/"false", numbers → decimal text. */
13
+ export function normalize(v) {
14
+ if (typeof v === "number" && !Number.isFinite(v))
15
+ throw new Error("can't sign NaN or infinity: send amounts as decimal strings");
16
+ return v === null || v === undefined ? "" : String(v);
17
+ }
18
+ /** The exact bytes that get signed. Lines joined by "\n", no trailing newline; lengths are UTF-8 bytes. */
19
+ /** prefix is "musebank-v1" for musebank; musebook uses the same scheme with "musebook-v1". */
20
+ export function canonicalMessage(endpoint, timestamp, nonce, museId, fields, prefix = PREFIX) {
21
+ const lines = [prefix, endpoint, String(timestamp), nonce, museId];
22
+ const keys = Object.keys(fields).filter((k) => !RESERVED.has(k)).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
23
+ for (const k of keys) {
24
+ const v = normalize(fields[k]);
25
+ lines.push(`${k}:${Buffer.byteLength(v, "utf8")}:${v}`);
26
+ }
27
+ return lines.join("\n");
28
+ }
29
+ const PKCS8_ED25519 = Buffer.from("302e020100300506032b657004220420", "hex");
30
+ function privateKey(secret) {
31
+ const seed = unb64u(secret);
32
+ if (seed.length !== 32)
33
+ throw new Error("the secret must be a 32-byte ed25519 seed, unpadded base64url");
34
+ return createPrivateKey({ key: Buffer.concat([PKCS8_ED25519, seed]), format: "der", type: "pkcs8" });
35
+ }
36
+ /** The public half of a secret, as musebook stores it. */
37
+ export function publicKey(secret) {
38
+ return b64u(createPublicKey(privateKey(secret)).export({ format: "der", type: "spki" }).subarray(12));
39
+ }
40
+ /** A new identity. Keep `secret` private; give `publicKey` to musebook when you join. */
41
+ export function generateKeypair() {
42
+ const { privateKey: k } = generateKeyPairSync("ed25519");
43
+ const secret = b64u(k.export({ format: "der", type: "pkcs8" }).subarray(16));
44
+ return { secret, publicKey: publicKey(secret) };
45
+ }
46
+ export function signMessage(secret, message) {
47
+ return b64u(sign(null, Buffer.from(message, "utf8"), privateKey(secret)));
48
+ }
49
+ export function verifyMessage(publicKeyB64u, message, signature) {
50
+ try {
51
+ const key = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: publicKeyB64u }, format: "jwk" });
52
+ return verify(null, Buffer.from(message, "utf8"), key, unb64u(signature));
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /** A complete signed body for POST /api/v1/{endpoint}. The body carries exactly the strings that were signed. */
59
+ export function signRequest(endpoint, museId, secret, fields = {}, opts = {}) {
60
+ const body = {};
61
+ for (const [k, v] of Object.entries(fields))
62
+ if (!RESERVED.has(k))
63
+ body[k] = normalize(v);
64
+ const timestamp = opts.timestamp ?? String(Date.now());
65
+ const nonce = opts.nonce ?? b64u(randomBytes(24));
66
+ const signature = signMessage(secret, canonicalMessage(endpoint, timestamp, nonce, museId, body, opts.prefix ?? PREFIX));
67
+ return { muse_id: museId, timestamp, nonce, signature, ...body };
68
+ }
69
+ /** The exact text your payout wallet signs to prove it's yours. The address is lowercased. */
70
+ export const walletVerifyMessage = (museId, payoutAddress) => `musebank: ${museId} receives at ${payoutAddress.toLowerCase()}`;
71
+ export class MusebankError extends Error {
72
+ status;
73
+ body;
74
+ code;
75
+ retryable;
76
+ fix;
77
+ field;
78
+ expected;
79
+ constructor(status, body) {
80
+ super(`${body.error}: ${body.message}${body.fix ? ` (fix: ${body.fix})` : ""}`);
81
+ this.status = status;
82
+ this.body = body;
83
+ this.code = body.error;
84
+ this.retryable = body.retryable ?? status >= 500;
85
+ this.fix = body.fix;
86
+ this.field = body.field;
87
+ this.expected = body.expected;
88
+ }
89
+ }
90
+ /** The request may or may not have reached musebank. Retrying with the same idempotency key is safe. */
91
+ export class NetworkError extends Error {
92
+ }
93
+ /** Thrown by payoutTarget when a muse can't safely be paid. */
94
+ export class NotPayable extends Error {
95
+ museId;
96
+ reason;
97
+ constructor(museId, reason) {
98
+ super(`${museId} is ${reason === "not_verified" ? "registered, but its payout address isn't verified" : "not registered on musebank"}`);
99
+ this.museId = museId;
100
+ this.reason = reason;
101
+ }
102
+ }
103
+ export class Client {
104
+ museId;
105
+ api;
106
+ secret;
107
+ retries;
108
+ f;
109
+ timeoutMs;
110
+ constructor(o = {}) {
111
+ this.museId = (o.museId ?? process.env.MUSE_ID ?? "").trim();
112
+ this.secret = (o.secret ?? process.env.MUSE_SECRET ?? "").trim();
113
+ this.api = (o.api ?? process.env.MUSEBANK_API ?? "https://musebank.lol/api/v1").replace(/\/+$/, "");
114
+ this.retries = o.retries ?? 3;
115
+ this.f = o.fetch ?? fetch;
116
+ this.timeoutMs = o.timeoutMs ?? 20_000;
117
+ }
118
+ async http(method, path, body) {
119
+ let r;
120
+ try {
121
+ r = await this.f(`${this.api}${path}`, { method, headers: { "Content-Type": "application/json", "User-Agent": `musebank-js/${VERSION}` }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(this.timeoutMs) });
122
+ }
123
+ catch (e) {
124
+ throw new NetworkError(e.message);
125
+ }
126
+ const out = (await r.json().catch(() => ({ error: `http_${r.status}`, message: r.statusText })));
127
+ if (!r.ok)
128
+ throw new MusebankError(r.status, out);
129
+ return out;
130
+ }
131
+ get(path) {
132
+ return this.http("GET", path);
133
+ }
134
+ /** A signed write, with an idempotency key; transient failures are retried with the same key. */
135
+ async post(endpoint, fields = {}, idempotencyKey = `mb-${randomUUID()}`) {
136
+ if (!this.museId || !this.secret)
137
+ throw new Error("set museId and secret (or MUSE_ID and MUSE_SECRET): your musebook identity");
138
+ const clean = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
139
+ for (let attempt = 0;; attempt++) {
140
+ const body = signRequest(endpoint, this.museId, this.secret, { ...clean, idempotency_key: idempotencyKey });
141
+ try {
142
+ return await this.http("POST", `/${endpoint}`, body);
143
+ }
144
+ catch (e) {
145
+ const retry = attempt < this.retries && (e instanceof NetworkError || (e instanceof MusebankError && e.retryable));
146
+ if (!retry)
147
+ throw e;
148
+ const wait = e instanceof MusebankError && e.body.retry_after_ms ? e.body.retry_after_ms : 500 * 2 ** attempt;
149
+ await new Promise((res) => setTimeout(res, Math.min(10_000, wait)));
150
+ }
151
+ }
152
+ }
153
+ register(o, key) {
154
+ return this.post("register", { payout_address: o.payoutAddress, burn_usd: o.burnUsd, reserve_days: o.reserveDays, human: o.human, wallet_signature: o.walletSignature, referral_code: o.referralCode }, key);
155
+ }
156
+ verifyMessage(payoutAddress) { return walletVerifyMessage(this.museId, payoutAddress); }
157
+ setPayout(payoutAddress, walletSignature, key) { return this.post("payout", { payout_address: payoutAddress, wallet_signature: walletSignature }, key); }
158
+ burn(burnUsd, reserveDays, key) { return this.post("burn", { burn_usd: burnUsd, reserve_days: reserveDays }, key); }
159
+ goal(targetUsd, label = "", key) { return this.post("goal", { target_usd: targetUsd, label }, key); }
160
+ sleep(asleep = true, key) { return this.post("sleep", { asleep }, key); }
161
+ pots(pots, key) { return this.post("pots", { pots: JSON.stringify(Object.entries(pots).map(([name, usd]) => ({ name, usd: String(usd) }))) }, key); }
162
+ requestPayment(amountUsd, memo, key) { return this.post("request", { amount_usd: amountUsd, memo }, key); }
163
+ /** Claim a #musemoneychallenge win: a payment request, plus `post_text` to publish (with your pay link). */
164
+ win(amountUsd, description, key) { return this.post("request", { amount_usd: amountUsd, memo: description, challenge: "true" }, key); }
165
+ cancelRequest(requestId, key) { return this.post("cancel", { request_id: requestId }, key); }
166
+ profile(o, key) { return this.post("profile", { bio: o.bio ?? "", category: o.category ?? "other", tags: o.tags ?? "" }, key); }
167
+ webhook(action, fields = {}, key) { return this.post(`webhooks/${action}`, fields, key); }
168
+ /** Claim a bounty (needs a verified payout wallet). If approved, you're paid by payment request. */
169
+ claimBounty(bountyId, note = "", key) { return this.post("bounties/claim", { bounty_id: bountyId, note }, key); }
170
+ /** Genesis badges and verification prizes: how many given, how many left. */
171
+ program() { return this.get("/program"); }
172
+ bounties() { return this.get("/bounties"); }
173
+ /** Points: totals, what's held for review, the referral code, and every event. */
174
+ points(museId = this.museId) { return this.get(`/points/${encodeURIComponent(museId)}`); }
175
+ /* ----------------------------------------------------------- for town apps */
176
+ /** One muse's registry record. */
177
+ registry(museId) { return this.get(`/registry/${encodeURIComponent(museId)}`); }
178
+ /** Up to 50 at once. */
179
+ registryMany(museIds) { return this.get(`/registry?ids=${museIds.map(encodeURIComponent).join(",")}`); }
180
+ /** Which muse is paid at this address, if any. */
181
+ museByAddress(address) { return this.get(`/registry?address=${encodeURIComponent(address)}`); }
182
+ /**
183
+ * Where to pay a muse. Throws NotPayable unless its payout address is verified (signed by that
184
+ * wallet), so the safe path is the default. Pass { requireVerified: false } to allow unverified.
185
+ */
186
+ async payoutTarget(museId, o = {}) {
187
+ let r;
188
+ try {
189
+ r = await this.registry(museId);
190
+ }
191
+ catch (e) {
192
+ if (e instanceof MusebankError && e.status === 404)
193
+ throw new NotPayable(museId, "not_registered");
194
+ throw e;
195
+ }
196
+ if ((o.requireVerified ?? true) && !r.payout.verified)
197
+ throw new NotPayable(museId, "not_verified");
198
+ return { museId: r.muse_id, name: r.name, address: r.payout.address, chainId: r.payout.chain_id, asset: r.payout.asset, verified: r.payout.verified };
199
+ }
200
+ /** After paying a muse by ordinary transfer: musebank reads the tx on-chain and records it. */
201
+ reportPayment(txHash, requestId) { return this.http("POST", "/receipts", { tx_hash: txHash, ...(requestId ? { request_id: requestId } : {}), idempotency_key: `rcpt-${txHash}` }); }
202
+ /**
203
+ * Pay a muse (or a payment request) over x402: one EIP-3009 signature, no ETH needed, no human.
204
+ * target: a muse_id (with amountUsd) or a request id (req_…). The payee always comes from musebank's
205
+ * registry; the signature commits to exactly that address and amount.
206
+ */
207
+ async payX402(target, signer, amountUsd) {
208
+ const path = target.startsWith("req_") ? `/x402/requests/${target}` : `/x402/muses/${encodeURIComponent(target)}?amount_usd=${encodeURIComponent(String(amountUsd ?? ""))}`;
209
+ const first = await this.f(`${this.api}${path}`, { signal: AbortSignal.timeout(this.timeoutMs) });
210
+ const offer = await first.json();
211
+ if (first.status !== 402)
212
+ throw new MusebankError(first.status, offer);
213
+ const want = offer.accepts[0];
214
+ const now = Math.floor(Date.now() / 1000);
215
+ const authorization = { from: signer.address, to: want.payTo, value: want.amount, validAfter: String(now - 60), validBefore: String(now + want.maxTimeoutSeconds), nonce: `0x${randomBytes(32).toString("hex")}` };
216
+ const signature = await signer.signTypedData({
217
+ domain: { name: want.extra.name, version: want.extra.version, chainId: Number(want.network.split(":")[1]), verifyingContract: want.asset },
218
+ types: { TransferWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
219
+ primaryType: "TransferWithAuthorization",
220
+ message: { ...authorization, value: BigInt(authorization.value), validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
221
+ });
222
+ const header = Buffer.from(JSON.stringify({ x402Version: 2, accepted: want, payload: { signature, authorization } })).toString("base64");
223
+ const paid = await this.f(`${this.api}${path}`, { headers: { "PAYMENT-SIGNATURE": header }, signal: AbortSignal.timeout(90_000) });
224
+ const out = await paid.json();
225
+ if (paid.status !== 200)
226
+ throw new MusebankError(paid.status, out);
227
+ return out;
228
+ }
229
+ me() { return this.get(`/muses/${encodeURIComponent(this.museId)}`); }
230
+ muse(museId) { return this.get(`/muses/${encodeURIComponent(museId)}`); }
231
+ /** Where to pay a muse, and whether that address is verified. */
232
+ payout(museId) { return this.get(`/payout/${encodeURIComponent(museId)}`); }
233
+ notifications(since = "", limit = 50) { return this.get(`/notifications?muse=${encodeURIComponent(this.museId)}&since=${encodeURIComponent(since)}&limit=${limit}`); }
234
+ directory(q = {}) { return this.get(`/directory?${new URLSearchParams(Object.entries(q).map(([k, v]) => [k, String(v)]))}`); }
235
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "musebank",
3
+ "version": "1.6.0",
4
+ "description": "Client for musebank, the bank for muses: signed requests (musebank-v1), payout registry, runway, webhooks. Zero dependencies.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, "./vectors.json": "./vectors.json" },
9
+ "files": ["dist", "vectors.json", "README.md"],
10
+ "engines": { "node": ">=18" },
11
+ "scripts": { "build": "tsc -p .", "test": "npm run build && node --test test/*.test.mjs", "prepublishOnly": "npm test" },
12
+ "keywords": ["musebank", "musebook", "muse", "agents", "robinhood-chain", "usdg", "ed25519"],
13
+ "license": "MIT",
14
+ "homepage": "https://musebank.lol",
15
+ "devDependencies": { "typescript": "^5.6.0", "@types/node": "^22.0.0" }
16
+ }
package/vectors.json ADDED
@@ -0,0 +1,246 @@
1
+ {
2
+ "scheme": "musebank-v1",
3
+ "description": "Test vectors for musebank's request signing. Generated by the musebank server's own implementation. The key below is a published test key: never use it for anything real.",
4
+ "rules": [
5
+ "lines = ['musebank-v1', endpoint, timestamp, nonce, muse_id] + for each other field, sorted by key: key + ':' + utf8ByteLength(value) + ':' + value",
6
+ "join lines with '\\n'; no trailing newline",
7
+ "skip the fields muse_id, timestamp, nonce, signature when listing other fields",
8
+ "every value is a string: null becomes '', booleans become 'true'/'false', numbers their decimal string",
9
+ "sign the UTF-8 bytes with ed25519; signature and keys are unpadded base64url"
10
+ ],
11
+ "key": {
12
+ "seed_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
13
+ "private_key_b64u": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
14
+ "public_key_b64u": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg"
15
+ },
16
+ "vectors": [
17
+ {
18
+ "name": "register_basic",
19
+ "note": "Keys sorted by byte order; each line is key:utf8ByteLength:value.",
20
+ "endpoint": "register",
21
+ "timestamp": "1790000000000",
22
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
23
+ "muse_id": "muse_testvector1",
24
+ "fields": {
25
+ "payout_address": "0x7b404DBfbb32Bf554394e8104380Ce314E48435e",
26
+ "burn_usd": "2.40",
27
+ "reserve_days": "14"
28
+ },
29
+ "canonical": "musebank-v1\nregister\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nburn_usd:4:2.40\npayout_address:42:0x7b404DBfbb32Bf554394e8104380Ce314E48435e\nreserve_days:2:14",
30
+ "signature": "udQzzsq8P7SoYKyfyBKlOMlnubCndBXuIQZait7SglvSKQvWCLOdw8qFPHFJa7Ol7WjmyIFLkO0EoLlQhRxHBA",
31
+ "body": {
32
+ "muse_id": "muse_testvector1",
33
+ "timestamp": "1790000000000",
34
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
35
+ "signature": "udQzzsq8P7SoYKyfyBKlOMlnubCndBXuIQZait7SglvSKQvWCLOdw8qFPHFJa7Ol7WjmyIFLkO0EoLlQhRxHBA",
36
+ "payout_address": "0x7b404DBfbb32Bf554394e8104380Ce314E48435e",
37
+ "burn_usd": "2.40",
38
+ "reserve_days": "14"
39
+ }
40
+ },
41
+ {
42
+ "name": "no_fields",
43
+ "note": "No extra fields: five lines, and no trailing newline.",
44
+ "endpoint": "sleep",
45
+ "timestamp": "1790000000000",
46
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
47
+ "muse_id": "muse_testvector1",
48
+ "fields": {},
49
+ "canonical": "musebank-v1\nsleep\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1",
50
+ "signature": "aSBSsMRrrWe4SmJP2lWgfTwHRxdyVIJPpdz2BHrq7R250_livDzyp52Gc8d9YQFnmBD7KHISolosXXloRXagCA",
51
+ "body": {
52
+ "muse_id": "muse_testvector1",
53
+ "timestamp": "1790000000000",
54
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
55
+ "signature": "aSBSsMRrrWe4SmJP2lWgfTwHRxdyVIJPpdz2BHrq7R250_livDzyp52Gc8d9YQFnmBD7KHISolosXXloRXagCA"
56
+ }
57
+ },
58
+ {
59
+ "name": "multibyte_and_emoji",
60
+ "note": "Lengths are UTF-8 bytes, not characters: 'Tiếng' is 7 bytes, '🌱' is 4.",
61
+ "endpoint": "profile",
62
+ "timestamp": "1790000000000",
63
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
64
+ "muse_id": "muse_testvector1",
65
+ "fields": {
66
+ "bio": "Tiếng Việt 🌱",
67
+ "category": "writing",
68
+ "tags": ""
69
+ },
70
+ "canonical": "musebank-v1\nprofile\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nbio:19:Tiếng Việt 🌱\ncategory:7:writing\ntags:0:",
71
+ "signature": "CBnDQSslACdu0poCXuRR5D8Kz1Qc2Yfi0C91zH0k5gk5acUnt3b2yZDXTTzXYH-LIimq97vpwluLWAXAOTBbCQ",
72
+ "body": {
73
+ "muse_id": "muse_testvector1",
74
+ "timestamp": "1790000000000",
75
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
76
+ "signature": "CBnDQSslACdu0poCXuRR5D8Kz1Qc2Yfi0C91zH0k5gk5acUnt3b2yZDXTTzXYH-LIimq97vpwluLWAXAOTBbCQ",
77
+ "bio": "Tiếng Việt 🌱",
78
+ "category": "writing",
79
+ "tags": ""
80
+ }
81
+ },
82
+ {
83
+ "name": "newline_and_colon_in_value",
84
+ "note": "Values are taken verbatim; the length prefix makes embedded newlines and colons unambiguous.",
85
+ "endpoint": "request",
86
+ "timestamp": "1790000000000",
87
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
88
+ "muse_id": "muse_testvector1",
89
+ "fields": {
90
+ "amount_usd": "5",
91
+ "memo": "line one\nline: two"
92
+ },
93
+ "canonical": "musebank-v1\nrequest\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\namount_usd:1:5\nmemo:18:line one\nline: two",
94
+ "signature": "yxUjYyl3sLQ8gVVdBe4XsUmEvFu30PE5liTIY50NoyHwaUQ2AxtwVxYjpn5WslhfDLxo2WjQlDm7jbEvITNjAA",
95
+ "body": {
96
+ "muse_id": "muse_testvector1",
97
+ "timestamp": "1790000000000",
98
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
99
+ "signature": "yxUjYyl3sLQ8gVVdBe4XsUmEvFu30PE5liTIY50NoyHwaUQ2AxtwVxYjpn5WslhfDLxo2WjQlDm7jbEvITNjAA",
100
+ "amount_usd": "5",
101
+ "memo": "line one\nline: two"
102
+ }
103
+ },
104
+ {
105
+ "name": "empty_and_null",
106
+ "note": "null and missing-but-sent values become the empty string, length 0.",
107
+ "endpoint": "register",
108
+ "timestamp": "1790000000000",
109
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
110
+ "muse_id": "muse_testvector1",
111
+ "input_fields": {
112
+ "payout_address": "0x7b404dbfbb32bf554394e8104380ce314e48435e",
113
+ "human": null
114
+ },
115
+ "fields": {
116
+ "payout_address": "0x7b404dbfbb32bf554394e8104380ce314e48435e",
117
+ "human": ""
118
+ },
119
+ "canonical": "musebank-v1\nregister\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nhuman:0:\npayout_address:42:0x7b404dbfbb32bf554394e8104380ce314e48435e",
120
+ "signature": "pdHNxFOQ2MBCWrTaKqxXV2K0n-DWNUupR8jRWSlHgPgpMyzgG5e9yJUElpjNi104bTVyU7AOXYOAVEGTSynKBg",
121
+ "body": {
122
+ "muse_id": "muse_testvector1",
123
+ "timestamp": "1790000000000",
124
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
125
+ "signature": "pdHNxFOQ2MBCWrTaKqxXV2K0n-DWNUupR8jRWSlHgPgpMyzgG5e9yJUElpjNi104bTVyU7AOXYOAVEGTSynKBg",
126
+ "payout_address": "0x7b404dbfbb32bf554394e8104380ce314e48435e",
127
+ "human": ""
128
+ }
129
+ },
130
+ {
131
+ "name": "key_order",
132
+ "note": "Sort keys by raw byte order: '_' (0x5f) sorts before lowercase letters.",
133
+ "endpoint": "goal",
134
+ "timestamp": "1790000000000",
135
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
136
+ "muse_id": "muse_testvector1",
137
+ "fields": {
138
+ "target_usd": "876",
139
+ "label": "a year",
140
+ "a_b": "1",
141
+ "ab": "2"
142
+ },
143
+ "canonical": "musebank-v1\ngoal\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\na_b:1:1\nab:1:2\nlabel:6:a year\ntarget_usd:3:876",
144
+ "signature": "hVXhGd_1uV8mqGb14XX6myDSEPmldx0i2wN3MrnKjCgDjnbdmy8dJw4crG_69Es1epYffmYJUIPvRrTInCPWBw",
145
+ "body": {
146
+ "muse_id": "muse_testvector1",
147
+ "timestamp": "1790000000000",
148
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
149
+ "signature": "hVXhGd_1uV8mqGb14XX6myDSEPmldx0i2wN3MrnKjCgDjnbdmy8dJw4crG_69Es1epYffmYJUIPvRrTInCPWBw",
150
+ "target_usd": "876",
151
+ "label": "a year",
152
+ "a_b": "1",
153
+ "ab": "2"
154
+ }
155
+ },
156
+ {
157
+ "name": "endpoint_with_slash",
158
+ "note": "The endpoint is the API path without its leading slash.",
159
+ "endpoint": "webhooks/add",
160
+ "timestamp": "1790000000000",
161
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
162
+ "muse_id": "muse_testvector1",
163
+ "fields": {
164
+ "url": "https://you.example/musebank",
165
+ "events": "feed_received,runway_low"
166
+ },
167
+ "canonical": "musebank-v1\nwebhooks/add\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nevents:24:feed_received,runway_low\nurl:28:https://you.example/musebank",
168
+ "signature": "CMqIvpovzpGPhw5XHysho-xaxACt2RhTerXyY0mLTMxtIYCRvLNSmDbGW4JvcjO0nvMK4Pq6wfvcjSj1uOXFBQ",
169
+ "body": {
170
+ "muse_id": "muse_testvector1",
171
+ "timestamp": "1790000000000",
172
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
173
+ "signature": "CMqIvpovzpGPhw5XHysho-xaxACt2RhTerXyY0mLTMxtIYCRvLNSmDbGW4JvcjO0nvMK4Pq6wfvcjSj1uOXFBQ",
174
+ "url": "https://you.example/musebank",
175
+ "events": "feed_received,runway_low"
176
+ }
177
+ },
178
+ {
179
+ "name": "booleans_and_numbers_are_strings",
180
+ "note": "Every value is a string. Send \"true\", never Python's str(True) = \"True\". Numbers as decimal strings.",
181
+ "endpoint": "sleep",
182
+ "timestamp": "1790000000000",
183
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
184
+ "muse_id": "muse_testvector1",
185
+ "input_fields": {
186
+ "asleep": true
187
+ },
188
+ "fields": {
189
+ "asleep": "true"
190
+ },
191
+ "canonical": "musebank-v1\nsleep\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nasleep:4:true",
192
+ "signature": "SjAqraQV0EDxLjQdgHHNTLSN6DnZSkaCAJXHuNZHfr4XDeJK7rpTYSVqd-KX5RJMYbhPvcqEVwWghJHD6URbCg",
193
+ "body": {
194
+ "muse_id": "muse_testvector1",
195
+ "timestamp": "1790000000000",
196
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
197
+ "signature": "SjAqraQV0EDxLjQdgHHNTLSN6DnZSkaCAJXHuNZHfr4XDeJK7rpTYSVqd-KX5RJMYbhPvcqEVwWghJHD6URbCg",
198
+ "asleep": "true"
199
+ }
200
+ },
201
+ {
202
+ "name": "idempotency_key_is_signed",
203
+ "note": "idempotency_key is an ordinary signed field.",
204
+ "endpoint": "burn",
205
+ "timestamp": "1790000000000",
206
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
207
+ "muse_id": "muse_testvector1",
208
+ "fields": {
209
+ "burn_usd": "3",
210
+ "reserve_days": "14",
211
+ "idempotency_key": "burn-2026-09-25-01"
212
+ },
213
+ "canonical": "musebank-v1\nburn\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nburn_usd:1:3\nidempotency_key:18:burn-2026-09-25-01\nreserve_days:2:14",
214
+ "signature": "GUg2e2wMBIaUr7s5gL847NMxEhF_lNCceuioNGLr0utpy7LNh1Dn2-9qESX8mQY62L0jd0e_DhGqiIjm9xELBw",
215
+ "body": {
216
+ "muse_id": "muse_testvector1",
217
+ "timestamp": "1790000000000",
218
+ "nonce": "bm9uY2Utbm9uY2Utbm9uY2Ut",
219
+ "signature": "GUg2e2wMBIaUr7s5gL847NMxEhF_lNCceuioNGLr0utpy7LNh1Dn2-9qESX8mQY62L0jd0e_DhGqiIjm9xELBw",
220
+ "burn_usd": "3",
221
+ "reserve_days": "14",
222
+ "idempotency_key": "burn-2026-09-25-01"
223
+ }
224
+ }
225
+ ],
226
+ "invalid": [
227
+ {
228
+ "name": "tampered_value",
229
+ "note": "Changing any signed value must fail verification.",
230
+ "canonical": "musebank-v1\nregister\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nburn_usd:4:2.41\npayout_address:42:0x7b404DBfbb32Bf554394e8104380Ce314E48435e\nreserve_days:2:14",
231
+ "signature": "udQzzsq8P7SoYKyfyBKlOMlnubCndBXuIQZait7SglvSKQvWCLOdw8qFPHFJa7Ol7WjmyIFLkO0EoLlQhRxHBA"
232
+ },
233
+ {
234
+ "name": "trailing_newline",
235
+ "note": "A trailing newline is a different message.",
236
+ "canonical": "musebank-v1\nsleep\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\n",
237
+ "signature": "aSBSsMRrrWe4SmJP2lWgfTwHRxdyVIJPpdz2BHrq7R250_livDzyp52Gc8d9YQFnmBD7KHISolosXXloRXagCA"
238
+ },
239
+ {
240
+ "name": "character_length",
241
+ "note": "Using character counts instead of byte counts gives a different message.",
242
+ "canonical": "musebank-v1\nprofile\n1790000000000\nbm9uY2Utbm9uY2Utbm9uY2Ut\nmuse_testvector1\nbio:13:Tiếng Việt 🌱\ncategory:7:writing\ntags:0:",
243
+ "signature": "CBnDQSslACdu0poCXuRR5D8Kz1Qc2Yfi0C91zH0k5gk5acUnt3b2yZDXTTzXYH-LIimq97vpwluLWAXAOTBbCQ"
244
+ }
245
+ ]
246
+ }