@payagent/wdk-protocol 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,41 @@
1
+ # `@payagent/wdk-protocol`
2
+
3
+ PayAgent protocol for Tether WDK. **Not a wallet.** You pass a WDK `WalletAccount`. This package never sees a seed.
4
+
5
+ v1: `preflight` + `pay` for a payment link, profile, or address. USDT/USDC (and catalog natives) on Ethereum, Base, Solana.
6
+
7
+ See `docs/wdk-status.md` for what is built and tested, and `docs/wdk-protocol-implementation.md` for the fail-closed rules.
8
+
9
+ ```ts
10
+ import { PayAgentProtocol } from "@payagent/wdk-protocol";
11
+
12
+ const protocol = new PayAgentProtocol(account, {
13
+ baseUrl: "https://www.payagent.co",
14
+ apiKey: process.env.PAYAGENT_API_KEY!,
15
+ agentId: "your-agent-id",
16
+ networkId: "ethereum",
17
+ });
18
+
19
+ const result = await protocol.pay({
20
+ type: "link",
21
+ linkUsername: "alice",
22
+ linkId: "link_public_id",
23
+ });
24
+ ```
25
+
26
+ If `pay` throws `RecordFailedError`, the transfer already landed. Call `protocol.record(...)` with the same `txHash` and `idempotencyKey`. Do not transfer again.
27
+
28
+ ## Staging script (real WDK, not the Next app)
29
+
30
+ This is how WDK is wired to PayAgent today: a sidecar script, not checkout.
31
+
32
+ ```bash
33
+ cp packages/wdk-protocol/env.example packages/wdk-protocol/.env
34
+ # fill seed, staging API key, agent id, USDT link
35
+ pnpm wdk-protocol:staging address
36
+ pnpm wdk-protocol:staging attach
37
+ pnpm wdk-protocol:staging preflight
38
+ WDK_STAGING_SEND=1 pnpm wdk-protocol:staging pay --send
39
+ ```
40
+
41
+ `pay` will not broadcast unless both `--send` and `WDK_STAGING_SEND=1` are set. Use a no-approval USDT policy on staging. Do not use a production treasury seed.
@@ -0,0 +1,2 @@
1
+ /** Exact human amount → token base units. Rejects float-unsafe numbers. */
2
+ export declare function toBaseUnits(amount: string | number, decimals: number): bigint;
package/dist/amount.js ADDED
@@ -0,0 +1,38 @@
1
+ import { PayAgentProtocolError } from "./errors.js";
2
+ /** Exact human amount → token base units. Rejects float-unsafe numbers. */
3
+ export function toBaseUnits(amount, decimals) {
4
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
5
+ throw new PayAgentProtocolError("BAD_DECIMALS", `Invalid decimals: ${decimals}`);
6
+ }
7
+ const human = typeof amount === "number"
8
+ ? formatNumberAsDecimalString(amount)
9
+ : amount.trim();
10
+ if (!human || human.startsWith("-") || human === "." || human === "+") {
11
+ throw new PayAgentProtocolError("BAD_AMOUNT", `Invalid amount: ${String(amount)}`);
12
+ }
13
+ const parts = human.split(".");
14
+ if (parts.length > 2) {
15
+ throw new PayAgentProtocolError("BAD_AMOUNT", `Invalid amount: ${human}`);
16
+ }
17
+ const whole = parts[0] === "" ? "0" : parts[0];
18
+ const fraction = parts[1] ?? "";
19
+ if (!/^\d+$/.test(whole) || (fraction.length > 0 && !/^\d+$/.test(fraction))) {
20
+ throw new PayAgentProtocolError("BAD_AMOUNT", `Invalid amount: ${human}`);
21
+ }
22
+ if (fraction.length > decimals) {
23
+ throw new PayAgentProtocolError("AMOUNT_PRECISION", `Amount ${human} has more than ${decimals} decimal places`);
24
+ }
25
+ const padded = fraction.padEnd(decimals, "0");
26
+ const base = 10n ** BigInt(decimals);
27
+ return BigInt(whole) * base + BigInt(padded || "0");
28
+ }
29
+ function formatNumberAsDecimalString(amount) {
30
+ if (!Number.isFinite(amount) || amount <= 0) {
31
+ throw new PayAgentProtocolError("BAD_AMOUNT", `Invalid amount: ${amount}`);
32
+ }
33
+ const text = amount.toString();
34
+ if (/e/i.test(text)) {
35
+ throw new PayAgentProtocolError("BAD_AMOUNT", "Pass the amount as a decimal string to avoid scientific notation");
36
+ }
37
+ return text;
38
+ }
@@ -0,0 +1,13 @@
1
+ export type CatalogAsset = {
2
+ networkId: string;
3
+ tokenId: string;
4
+ decimals: number;
5
+ kind: "erc20" | "spl" | "native";
6
+ address?: string;
7
+ };
8
+ export declare function lookupAsset(networkId: string, tokenId: string): CatalogAsset;
9
+ export declare function isEvmNetwork(networkId: string): networkId is "ethereum" | "base" | "sepolia";
10
+ export declare function isSolanaNetwork(networkId: string): networkId is "solana";
11
+ export declare function isEvmAddress(address: string): boolean;
12
+ export declare function isSolanaAddress(address: string): boolean;
13
+ export declare function sameAddress(a: string, b: string, networkId: string): boolean;
@@ -0,0 +1,102 @@
1
+ import { PayAgentProtocolError } from "./errors.js";
2
+ const catalog = [
3
+ {
4
+ networkId: "ethereum",
5
+ tokenId: "usdt",
6
+ decimals: 6,
7
+ kind: "erc20",
8
+ address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
9
+ },
10
+ {
11
+ networkId: "ethereum",
12
+ tokenId: "usdc",
13
+ decimals: 6,
14
+ kind: "erc20",
15
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
16
+ },
17
+ {
18
+ networkId: "ethereum",
19
+ tokenId: "lcx",
20
+ decimals: 18,
21
+ kind: "erc20",
22
+ address: "0x8CD41041505885EF0aD3858181D66f17BE8aae7E",
23
+ },
24
+ { networkId: "ethereum", tokenId: "eth", decimals: 18, kind: "native" },
25
+ {
26
+ networkId: "base",
27
+ tokenId: "usdt",
28
+ decimals: 6,
29
+ kind: "erc20",
30
+ address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
31
+ },
32
+ {
33
+ networkId: "base",
34
+ tokenId: "usdc",
35
+ decimals: 6,
36
+ kind: "erc20",
37
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
38
+ },
39
+ { networkId: "base", tokenId: "eth", decimals: 18, kind: "native" },
40
+ {
41
+ networkId: "solana",
42
+ tokenId: "usdt",
43
+ decimals: 6,
44
+ kind: "spl",
45
+ address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
46
+ },
47
+ {
48
+ networkId: "solana",
49
+ tokenId: "usdc",
50
+ decimals: 6,
51
+ kind: "spl",
52
+ address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
53
+ },
54
+ { networkId: "solana", tokenId: "sol", decimals: 9, kind: "native" },
55
+ {
56
+ networkId: "sepolia",
57
+ tokenId: "usdt",
58
+ decimals: 6,
59
+ kind: "erc20",
60
+ address: "0xF9E0643Ba46eeaf4e1059775567f67F5c867bbfc",
61
+ },
62
+ {
63
+ networkId: "sepolia",
64
+ tokenId: "usdc",
65
+ decimals: 6,
66
+ kind: "erc20",
67
+ address: "0x3402d41AA8e34e0DF605c12109de2f8F4FF33A87",
68
+ },
69
+ {
70
+ networkId: "sepolia",
71
+ tokenId: "lcx",
72
+ decimals: 18,
73
+ kind: "erc20",
74
+ address: "0x98d99c88D31C27C5a591Fe7F023F9DB0B37E4B3b",
75
+ },
76
+ { networkId: "sepolia", tokenId: "eth", decimals: 18, kind: "native" },
77
+ ];
78
+ export function lookupAsset(networkId, tokenId) {
79
+ const asset = catalog.find((item) => item.networkId === networkId && item.tokenId === tokenId);
80
+ if (!asset) {
81
+ throw new PayAgentProtocolError("UNSUPPORTED_ASSET", `Unsupported token ${tokenId} on ${networkId}`);
82
+ }
83
+ return asset;
84
+ }
85
+ export function isEvmNetwork(networkId) {
86
+ return networkId === "ethereum" || networkId === "base" || networkId === "sepolia";
87
+ }
88
+ export function isSolanaNetwork(networkId) {
89
+ return networkId === "solana";
90
+ }
91
+ export function isEvmAddress(address) {
92
+ return /^0x[a-fA-F0-9]{40}$/.test(address.trim());
93
+ }
94
+ export function isSolanaAddress(address) {
95
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address.trim());
96
+ }
97
+ export function sameAddress(a, b, networkId) {
98
+ if (isEvmNetwork(networkId)) {
99
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
100
+ }
101
+ return a.trim() === b.trim();
102
+ }
@@ -0,0 +1,13 @@
1
+ export type HttpClient = {
2
+ get(path: string, query?: Record<string, string | undefined>): Promise<unknown>;
3
+ post(path: string, body: unknown, headers?: Record<string, string>): Promise<{
4
+ status: number;
5
+ body: unknown;
6
+ }>;
7
+ };
8
+ export declare function createHttpClient(input: {
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ fetchImpl: typeof fetch;
12
+ }): HttpClient;
13
+ export declare function newIdempotencyKey(): string;
package/dist/client.js ADDED
@@ -0,0 +1,69 @@
1
+ import { PayAgentProtocolError } from "./errors.js";
2
+ export function createHttpClient(input) {
3
+ const baseUrl = input.baseUrl.replace(/\/$/, "");
4
+ async function request(path, init) {
5
+ const headers = new Headers(init.headers);
6
+ if (!headers.has("Authorization")) {
7
+ headers.set("Authorization", `Bearer ${input.apiKey}`);
8
+ }
9
+ if (init.body && !headers.has("Content-Type")) {
10
+ headers.set("Content-Type", "application/json");
11
+ }
12
+ const response = await input.fetchImpl(`${baseUrl}${path}`, {
13
+ ...init,
14
+ headers,
15
+ });
16
+ const text = await response.text();
17
+ let body = null;
18
+ if (text) {
19
+ try {
20
+ body = JSON.parse(text);
21
+ }
22
+ catch {
23
+ body = text;
24
+ }
25
+ }
26
+ return { status: response.status, body };
27
+ }
28
+ return {
29
+ async get(path, query = {}) {
30
+ const params = new URLSearchParams();
31
+ for (const [key, value] of Object.entries(query)) {
32
+ if (value != null && value !== "")
33
+ params.set(key, value);
34
+ }
35
+ const suffix = params.size > 0 ? `?${params}` : "";
36
+ const { status, body } = await request(`${path}${suffix}`, { method: "GET" });
37
+ if (status < 200 || status >= 300) {
38
+ const message = typeof body === "object" && body && "error" in body
39
+ ? String(body.error)
40
+ : `GET ${path} failed (${status})`;
41
+ throw new PayAgentProtocolError("HTTP_ERROR", message);
42
+ }
43
+ return body;
44
+ },
45
+ async post(path, body, extraHeaders = {}) {
46
+ return request(path, {
47
+ method: "POST",
48
+ body: JSON.stringify(body),
49
+ headers: extraHeaders,
50
+ });
51
+ },
52
+ };
53
+ }
54
+ export function newIdempotencyKey() {
55
+ if (typeof globalThis.crypto?.randomUUID === "function") {
56
+ return globalThis.crypto.randomUUID();
57
+ }
58
+ const bytes = new Uint8Array(16);
59
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
60
+ globalThis.crypto.getRandomValues(bytes);
61
+ }
62
+ else {
63
+ throw new PayAgentProtocolError("NO_CRYPTO", "crypto.randomUUID is required to build an Idempotency-Key");
64
+ }
65
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
66
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
67
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
68
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
69
+ }
@@ -0,0 +1,21 @@
1
+ export declare class PayAgentProtocolError extends Error {
2
+ readonly code: string;
3
+ constructor(code: string, message: string);
4
+ }
5
+ export declare class PreflightDeniedError extends PayAgentProtocolError {
6
+ readonly checks: unknown;
7
+ constructor(message: string, checks: unknown);
8
+ }
9
+ export declare class RecordFailedError extends PayAgentProtocolError {
10
+ readonly txHash: string;
11
+ readonly idempotencyKey: string;
12
+ readonly status: number;
13
+ readonly body: unknown;
14
+ constructor(input: {
15
+ txHash: string;
16
+ idempotencyKey: string;
17
+ status: number;
18
+ body: unknown;
19
+ message?: string;
20
+ });
21
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,31 @@
1
+ export class PayAgentProtocolError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = "PayAgentProtocolError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ export class PreflightDeniedError extends PayAgentProtocolError {
10
+ checks;
11
+ constructor(message, checks) {
12
+ super("PREFLIGHT_DENIED", message);
13
+ this.name = "PreflightDeniedError";
14
+ this.checks = checks;
15
+ }
16
+ }
17
+ export class RecordFailedError extends PayAgentProtocolError {
18
+ txHash;
19
+ idempotencyKey;
20
+ status;
21
+ body;
22
+ constructor(input) {
23
+ super("RECORD_FAILED", input.message ??
24
+ `PayAgent did not record tx ${input.txHash} (HTTP ${input.status}). Retry record() with the same hash and idempotency key. Do not transfer again.`);
25
+ this.name = "RecordFailedError";
26
+ this.txHash = input.txHash;
27
+ this.idempotencyKey = input.idempotencyKey;
28
+ this.status = input.status;
29
+ this.body = input.body;
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ import type { WdkWalletAccount } from "./types.js";
2
+ export declare function signAgentChallenge(account: WdkWalletAccount, message: string): Promise<string>;
@@ -0,0 +1,11 @@
1
+ import { PayAgentProtocolError } from "./errors.js";
2
+ export async function signAgentChallenge(account, message) {
3
+ if (typeof account.sign !== "function") {
4
+ throw new PayAgentProtocolError("NO_SIGN", "WalletAccount.sign(message) is required to prove an agent challenge");
5
+ }
6
+ const signature = await account.sign(message);
7
+ if (!signature?.trim()) {
8
+ throw new PayAgentProtocolError("NO_SIGNATURE", "WalletAccount.sign returned no signature");
9
+ }
10
+ return signature.trim();
11
+ }
@@ -0,0 +1,6 @@
1
+ export { toBaseUnits } from "./amount.js";
2
+ export { lookupAsset } from "./catalog.js";
3
+ export { PayAgentProtocolError, PreflightDeniedError, RecordFailedError, } from "./errors.js";
4
+ export { signAgentChallenge } from "./identity.js";
5
+ export { PayAgentProtocol } from "./protocol.js";
6
+ export type { AddressPayIntent, AgentChallenge, AgentProof, Destination, LinkPayIntent, PayAgentProtocolConfig, PayIntent, PayResult, PreflightResult, ProfilePayIntent, WdkWalletAccount, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { toBaseUnits } from "./amount.js";
2
+ export { lookupAsset } from "./catalog.js";
3
+ export { PayAgentProtocolError, PreflightDeniedError, RecordFailedError, } from "./errors.js";
4
+ export { signAgentChallenge } from "./identity.js";
5
+ export { PayAgentProtocol } from "./protocol.js";
@@ -0,0 +1,28 @@
1
+ import type { AgentChallenge, AgentProof, Destination, PayAgentProtocolConfig, PayIntent, PayResult, PreflightResult, WdkWalletAccount } from "./types.js";
2
+ export declare class PayAgentProtocol {
3
+ readonly account: WdkWalletAccount;
4
+ readonly config: Required<Pick<PayAgentProtocolConfig, "apiKey" | "agentId" | "networkId">> & {
5
+ baseUrl: string;
6
+ };
7
+ private readonly http;
8
+ constructor(account: WdkWalletAccount, config: PayAgentProtocolConfig);
9
+ preflight(intent: PayIntent): Promise<PreflightResult>;
10
+ pay(intent: PayIntent): Promise<PayResult>;
11
+ record(input: {
12
+ intent: PayIntent;
13
+ destination: Destination;
14
+ payerAddress: string;
15
+ txHash: string;
16
+ idempotencyKey: string;
17
+ }): Promise<PayResult>;
18
+ resolvePayee(intent: PayIntent): Promise<Destination>;
19
+ private preflightForPayer;
20
+ private transfer;
21
+ private intentNetwork;
22
+ private assertAccountFitsNetwork;
23
+ private assertDestinationFitsAccount;
24
+ issueCredential(ttlHours?: number): Promise<unknown>;
25
+ getPublicCredential(publicId: string): Promise<unknown>;
26
+ createChallenge(publicId: string): Promise<AgentChallenge>;
27
+ prove(publicId: string): Promise<AgentProof>;
28
+ }
@@ -0,0 +1,413 @@
1
+ import { toBaseUnits } from "./amount.js";
2
+ import { isEvmAddress, isEvmNetwork, isSolanaAddress, isSolanaNetwork, lookupAsset, sameAddress, } from "./catalog.js";
3
+ import { createHttpClient, newIdempotencyKey } from "./client.js";
4
+ import { PayAgentProtocolError, PreflightDeniedError, RecordFailedError, } from "./errors.js";
5
+ import { signAgentChallenge } from "./identity.js";
6
+ const APPROVAL_REQUIRED = "APPROVAL_REQUIRED";
7
+ export class PayAgentProtocol {
8
+ account;
9
+ config;
10
+ http;
11
+ constructor(account, config) {
12
+ if (!account?.getAddress || !account.transfer) {
13
+ throw new PayAgentProtocolError("NO_ACCOUNT", "PayAgentProtocol requires a WDK WalletAccount (getAddress + transfer)");
14
+ }
15
+ if (!config.apiKey?.trim()) {
16
+ throw new PayAgentProtocolError("NO_API_KEY", "apiKey is required");
17
+ }
18
+ if (!config.agentId?.trim()) {
19
+ throw new PayAgentProtocolError("NO_AGENT_ID", "agentId is required");
20
+ }
21
+ if (!config.networkId?.trim()) {
22
+ throw new PayAgentProtocolError("NO_NETWORK", "networkId is required");
23
+ }
24
+ this.account = account;
25
+ this.config = {
26
+ baseUrl: (config.baseUrl ?? "https://www.payagent.co").replace(/\/$/, ""),
27
+ apiKey: config.apiKey.trim(),
28
+ agentId: config.agentId.trim(),
29
+ networkId: config.networkId.trim(),
30
+ };
31
+ this.http = createHttpClient({
32
+ baseUrl: this.config.baseUrl,
33
+ apiKey: this.config.apiKey,
34
+ fetchImpl: config.fetch ?? globalThis.fetch.bind(globalThis),
35
+ });
36
+ }
37
+ async preflight(intent) {
38
+ const payerAddress = await this.account.getAddress();
39
+ this.assertAccountFitsNetwork(payerAddress, this.intentNetwork(intent));
40
+ return this.preflightForPayer(intent, payerAddress);
41
+ }
42
+ async pay(intent) {
43
+ const payerAddress = await this.account.getAddress();
44
+ const networkId = this.intentNetwork(intent);
45
+ this.assertAccountFitsNetwork(payerAddress, networkId);
46
+ const idempotencyKey = newIdempotencyKey();
47
+ const preflight = await this.preflightForPayer(intent, payerAddress);
48
+ if (hasApprovalRequired(preflight.checks) && !intent.approvalId) {
49
+ return {
50
+ status: "approval_required",
51
+ checks: preflight.checks,
52
+ approvalId: approvalIdFromChecks(preflight.checks),
53
+ };
54
+ }
55
+ if (!preflight.ready) {
56
+ throw new PreflightDeniedError("PayAgent preflight is not ready. No transfer was sent.", preflight.checks);
57
+ }
58
+ const destination = await this.resolvePayee(intent);
59
+ this.assertDestinationFitsAccount(payerAddress, destination);
60
+ const asset = lookupAsset(destination.networkId, destination.tokenId);
61
+ const units = toBaseUnits(destination.amount, asset.decimals);
62
+ if (units <= 0n) {
63
+ throw new PayAgentProtocolError("BAD_AMOUNT", "Amount must be greater than zero");
64
+ }
65
+ const txHash = await this.transfer(destination.recipientAddress, asset, units);
66
+ if (typeof this.account.waitForTransaction === "function") {
67
+ try {
68
+ await this.account.waitForTransaction(txHash, { target: "confirmed" });
69
+ }
70
+ catch {
71
+ // Hash exists. Record anyway; PayAgent verify may need a retry.
72
+ }
73
+ }
74
+ return this.record({
75
+ intent,
76
+ destination,
77
+ payerAddress,
78
+ txHash,
79
+ idempotencyKey,
80
+ });
81
+ }
82
+ async record(input) {
83
+ const body = recordBody(this.config.agentId, input.payerAddress, input.txHash, input.intent, input.destination);
84
+ const { status, body: response } = await this.http.post("/api/v1/pay", body, {
85
+ "Idempotency-Key": input.idempotencyKey,
86
+ });
87
+ if (status === 202) {
88
+ const approvalId = readApprovalId(response);
89
+ return {
90
+ status: "approval_required",
91
+ txHash: input.txHash,
92
+ idempotencyKey: input.idempotencyKey,
93
+ approvalId,
94
+ checks: isRecord(response) && isRecord(response.checks)
95
+ ? response.checks
96
+ : undefined,
97
+ };
98
+ }
99
+ if (status < 200 || status >= 300) {
100
+ throw new RecordFailedError({
101
+ txHash: input.txHash,
102
+ idempotencyKey: input.idempotencyKey,
103
+ status,
104
+ body: response,
105
+ });
106
+ }
107
+ return {
108
+ status: "paid",
109
+ txHash: input.txHash,
110
+ idempotencyKey: input.idempotencyKey,
111
+ destination: input.destination,
112
+ record: response,
113
+ };
114
+ }
115
+ async resolvePayee(intent) {
116
+ if (intent.type === "link") {
117
+ const link = await this.http.get(`/api/pay/${encodeURIComponent(intent.linkUsername)}/${encodeURIComponent(intent.linkId)}`);
118
+ const parsed = parsePublicLink(link);
119
+ if (!parsed.recipientAddress) {
120
+ throw new PayAgentProtocolError("NO_RECIPIENT", "Payment link has no recipient address");
121
+ }
122
+ if (parsed.canPay === false || (parsed.status && parsed.status !== "pending")) {
123
+ throw new PayAgentProtocolError("LINK_NOT_PAYABLE", `Payment link is not payable (${parsed.status ?? "unknown"})`);
124
+ }
125
+ return {
126
+ type: "link",
127
+ recipientAddress: parsed.recipientAddress,
128
+ amount: parsed.amount,
129
+ tokenId: parsed.tokenId,
130
+ networkId: parsed.networkId,
131
+ linkUsername: parsed.username,
132
+ linkId: parsed.publicId,
133
+ };
134
+ }
135
+ if (intent.type === "profile") {
136
+ const payload = await this.http.get(`/api/public/users/${encodeURIComponent(intent.recipientUsername)}`);
137
+ const profile = parsePublicProfile(payload);
138
+ const wallet = profile.wallets.find((item) => item.networkId === intent.networkId);
139
+ if (!wallet?.address) {
140
+ throw new PayAgentProtocolError("NO_RECIPIENT", `Recipient has no verified wallet on ${intent.networkId}`);
141
+ }
142
+ return {
143
+ type: "profile",
144
+ recipientAddress: wallet.address,
145
+ amount: intent.amount,
146
+ tokenId: intent.tokenId,
147
+ networkId: intent.networkId,
148
+ recipientUsername: profile.username,
149
+ };
150
+ }
151
+ return {
152
+ type: "address",
153
+ recipientAddress: intent.recipientAddress,
154
+ amount: intent.amount,
155
+ tokenId: intent.tokenId,
156
+ networkId: intent.networkId,
157
+ };
158
+ }
159
+ async preflightForPayer(intent, payerAddress) {
160
+ const query = {
161
+ type: intent.type,
162
+ agentId: this.config.agentId,
163
+ payerAddress,
164
+ };
165
+ if (intent.type === "link") {
166
+ query.linkUsername = intent.linkUsername;
167
+ query.linkId = intent.linkId;
168
+ }
169
+ else if (intent.type === "profile") {
170
+ query.recipientUsername = intent.recipientUsername;
171
+ query.tokenId = intent.tokenId;
172
+ query.networkId = intent.networkId;
173
+ query.amount = String(intent.amount);
174
+ }
175
+ else {
176
+ query.recipientAddress = intent.recipientAddress;
177
+ query.tokenId = intent.tokenId;
178
+ query.networkId = intent.networkId;
179
+ query.amount = String(intent.amount);
180
+ }
181
+ const body = await this.http.get("/api/v1/pay/preflight", query);
182
+ return parsePreflight(body);
183
+ }
184
+ async transfer(recipient, asset, units) {
185
+ if (asset.kind === "native") {
186
+ if (typeof this.account.sendTransaction !== "function") {
187
+ throw new PayAgentProtocolError("NO_NATIVE_SEND", "This WalletAccount cannot send native ETH/SOL. Fund a token pay or pass sendTransaction.");
188
+ }
189
+ const result = await this.account.sendTransaction({
190
+ to: recipient,
191
+ value: units,
192
+ });
193
+ return readHash(result);
194
+ }
195
+ if (!asset.address) {
196
+ throw new PayAgentProtocolError("NO_TOKEN", `Missing contract/mint for ${asset.tokenId}`);
197
+ }
198
+ const result = await this.account.transfer({
199
+ token: asset.address,
200
+ recipient,
201
+ amount: units,
202
+ });
203
+ return readHash(result);
204
+ }
205
+ intentNetwork(intent) {
206
+ if (intent.type === "link")
207
+ return this.config.networkId;
208
+ return intent.networkId;
209
+ }
210
+ assertAccountFitsNetwork(payerAddress, networkId) {
211
+ if (isEvmNetwork(networkId) && !isEvmAddress(payerAddress)) {
212
+ throw new PayAgentProtocolError("NETWORK_MISMATCH", `WalletAccount ${payerAddress} is not an EVM address for ${networkId}`);
213
+ }
214
+ if (isSolanaNetwork(networkId) && !isSolanaAddress(payerAddress)) {
215
+ throw new PayAgentProtocolError("NETWORK_MISMATCH", `WalletAccount ${payerAddress} is not a Solana address for ${networkId}`);
216
+ }
217
+ if (isEvmNetwork(this.config.networkId) !== isEvmNetwork(networkId)) {
218
+ throw new PayAgentProtocolError("NETWORK_MISMATCH", `Protocol is bound to ${this.config.networkId}, intent uses ${networkId}`);
219
+ }
220
+ }
221
+ assertDestinationFitsAccount(payerAddress, destination) {
222
+ this.assertAccountFitsNetwork(payerAddress, destination.networkId);
223
+ if (destination.networkId !== this.config.networkId) {
224
+ throw new PayAgentProtocolError("NETWORK_MISMATCH", `Destination network ${destination.networkId} does not match protocol ${this.config.networkId}`);
225
+ }
226
+ if (isEvmNetwork(destination.networkId) &&
227
+ !isEvmAddress(destination.recipientAddress)) {
228
+ throw new PayAgentProtocolError("BAD_RECIPIENT", "Recipient is not a valid EVM address");
229
+ }
230
+ if (isSolanaNetwork(destination.networkId) &&
231
+ !isSolanaAddress(destination.recipientAddress)) {
232
+ throw new PayAgentProtocolError("BAD_RECIPIENT", "Recipient is not a valid Solana address");
233
+ }
234
+ if (sameAddress(payerAddress, destination.recipientAddress, destination.networkId)) {
235
+ throw new PayAgentProtocolError("SELF_PAY", "Payer and recipient addresses are the same");
236
+ }
237
+ }
238
+ async issueCredential(ttlHours) {
239
+ const { status, body } = await this.http.post("/api/v1/agents/credentials", {
240
+ agentId: this.config.agentId,
241
+ ttlHours,
242
+ });
243
+ if (status < 200 || status >= 300) {
244
+ throw new PayAgentProtocolError("CREDENTIAL_ISSUE_FAILED", httpError(body, status));
245
+ }
246
+ return body;
247
+ }
248
+ async getPublicCredential(publicId) {
249
+ return this.http.get(`/agents/${encodeURIComponent(publicId.trim())}/credential.json`);
250
+ }
251
+ async createChallenge(publicId) {
252
+ const { status, body } = await this.http.post("/api/public/agent-challenge", {
253
+ publicId: publicId.trim(),
254
+ networkId: this.config.networkId,
255
+ });
256
+ if (status < 200 || status >= 300) {
257
+ throw new PayAgentProtocolError("CHALLENGE_FAILED", httpError(body, status));
258
+ }
259
+ if (!isRecord(body) ||
260
+ typeof body.challengeId !== "string" ||
261
+ typeof body.message !== "string" ||
262
+ typeof body.publicId !== "string" ||
263
+ typeof body.networkId !== "string" ||
264
+ typeof body.expiresAt !== "string") {
265
+ throw new PayAgentProtocolError("CHALLENGE_FAILED", "Unexpected challenge response");
266
+ }
267
+ return {
268
+ challengeId: body.challengeId,
269
+ publicId: body.publicId,
270
+ networkId: body.networkId,
271
+ message: body.message,
272
+ expiresAt: body.expiresAt,
273
+ };
274
+ }
275
+ async prove(publicId) {
276
+ const address = await this.account.getAddress();
277
+ const challenge = await this.createChallenge(publicId);
278
+ const signature = await signAgentChallenge(this.account, challenge.message);
279
+ const { status, body } = await this.http.post("/api/public/agent-challenge/verify", {
280
+ challengeId: challenge.challengeId,
281
+ address,
282
+ signature,
283
+ });
284
+ if (status < 200 || status >= 300) {
285
+ throw new PayAgentProtocolError("PROVE_FAILED", httpError(body, status));
286
+ }
287
+ if (!isRecord(body) || body.ok !== true || typeof body.publicId !== "string") {
288
+ throw new PayAgentProtocolError("PROVE_FAILED", "Unexpected verify response");
289
+ }
290
+ return {
291
+ ok: true,
292
+ publicId: body.publicId,
293
+ address: typeof body.address === "string" ? body.address : address,
294
+ networkId: typeof body.networkId === "string" ? body.networkId : this.config.networkId,
295
+ credential: body.credential,
296
+ };
297
+ }
298
+ }
299
+ function httpError(body, status) {
300
+ if (isRecord(body) && typeof body.error === "string")
301
+ return body.error;
302
+ return `Request failed (${status})`;
303
+ }
304
+ function parsePreflight(body) {
305
+ if (!isRecord(body) || typeof body.ready !== "boolean" || !isRecord(body.checks)) {
306
+ throw new PayAgentProtocolError("BAD_PREFLIGHT", "Unexpected preflight response");
307
+ }
308
+ return {
309
+ ready: body.ready,
310
+ type: body.type === "profile" || body.type === "address" ? body.type : "link",
311
+ checks: body.checks,
312
+ };
313
+ }
314
+ function parsePublicLink(body) {
315
+ const link = isRecord(body) && isRecord(body.link) ? body.link : body;
316
+ if (!isRecord(link) ||
317
+ typeof link.username !== "string" ||
318
+ typeof link.publicId !== "string" ||
319
+ typeof link.tokenId !== "string" ||
320
+ typeof link.networkId !== "string" ||
321
+ (typeof link.amount !== "number" && typeof link.amount !== "string")) {
322
+ throw new PayAgentProtocolError("BAD_LINK", "Unexpected payment link response");
323
+ }
324
+ return {
325
+ username: link.username,
326
+ publicId: link.publicId,
327
+ amount: typeof link.amount === "number" ? link.amount : Number(link.amount),
328
+ tokenId: link.tokenId,
329
+ networkId: link.networkId,
330
+ recipientAddress: typeof link.recipientAddress === "string" ? link.recipientAddress : null,
331
+ canPay: typeof link.canPay === "boolean" ? link.canPay : undefined,
332
+ status: typeof link.status === "string" ? link.status : undefined,
333
+ };
334
+ }
335
+ function parsePublicProfile(body) {
336
+ const profile = isRecord(body) && isRecord(body.profile) ? body.profile : body;
337
+ if (!isRecord(profile) || typeof profile.username !== "string" || !Array.isArray(profile.wallets)) {
338
+ throw new PayAgentProtocolError("BAD_PROFILE", "Unexpected profile response");
339
+ }
340
+ return {
341
+ username: profile.username,
342
+ wallets: profile.wallets
343
+ .filter(isRecord)
344
+ .map((wallet) => ({
345
+ networkId: String(wallet.networkId ?? ""),
346
+ address: String(wallet.address ?? ""),
347
+ }))
348
+ .filter((wallet) => wallet.networkId && wallet.address),
349
+ };
350
+ }
351
+ function recordBody(agentId, payerAddress, txHash, intent, destination) {
352
+ const base = {
353
+ agentId,
354
+ payerAddress,
355
+ txHash,
356
+ type: destination.type,
357
+ approvalId: intent.approvalId,
358
+ };
359
+ if (destination.type === "link") {
360
+ return {
361
+ ...base,
362
+ linkUsername: destination.linkUsername,
363
+ linkId: destination.linkId,
364
+ };
365
+ }
366
+ if (destination.type === "profile") {
367
+ return {
368
+ ...base,
369
+ recipientUsername: destination.recipientUsername,
370
+ amount: Number(destination.amount),
371
+ tokenId: destination.tokenId,
372
+ networkId: destination.networkId,
373
+ };
374
+ }
375
+ return {
376
+ ...base,
377
+ recipientAddress: destination.recipientAddress,
378
+ amount: Number(destination.amount),
379
+ tokenId: destination.tokenId,
380
+ networkId: destination.networkId,
381
+ };
382
+ }
383
+ function hasApprovalRequired(checks) {
384
+ return Object.values(checks).some((check) => check.code === APPROVAL_REQUIRED);
385
+ }
386
+ function approvalIdFromChecks(checks) {
387
+ for (const check of Object.values(checks)) {
388
+ const match = /approvals\/([A-Za-z0-9]+)/.exec(check.message ?? "");
389
+ if (match?.[1])
390
+ return match[1];
391
+ }
392
+ return undefined;
393
+ }
394
+ function readApprovalId(body) {
395
+ if (!isRecord(body))
396
+ return undefined;
397
+ if (typeof body.approvalId === "string")
398
+ return body.approvalId;
399
+ if (isRecord(body.error) && typeof body.error.approvalId === "string") {
400
+ return body.error.approvalId;
401
+ }
402
+ return undefined;
403
+ }
404
+ function readHash(result) {
405
+ const hash = typeof result === "string" ? result : result.hash;
406
+ if (!hash?.trim()) {
407
+ throw new PayAgentProtocolError("NO_TX_HASH", "WalletAccount transfer returned no hash");
408
+ }
409
+ return hash.trim();
410
+ }
411
+ function isRecord(value) {
412
+ return typeof value === "object" && value !== null;
413
+ }
@@ -0,0 +1,117 @@
1
+ export type PayType = "link" | "profile" | "address";
2
+ export type WdkTransferResult = {
3
+ hash: string;
4
+ };
5
+ export type WdkWalletAccount = {
6
+ getAddress(): Promise<string>;
7
+ transfer(options: {
8
+ token: string;
9
+ recipient: string;
10
+ amount: bigint;
11
+ }): Promise<WdkTransferResult>;
12
+ sendTransaction?(tx: {
13
+ to: string;
14
+ value: bigint;
15
+ }): Promise<WdkTransferResult | string>;
16
+ waitForTransaction?(hash: string, options?: {
17
+ target?: "confirmed" | "final";
18
+ }): Promise<unknown>;
19
+ /** Sign a plaintext challenge. Required only for prove(), not for pay(). */
20
+ sign?(message: string): Promise<string>;
21
+ };
22
+ export type AgentChallenge = {
23
+ challengeId: string;
24
+ publicId: string;
25
+ networkId: string;
26
+ message: string;
27
+ expiresAt: string;
28
+ };
29
+ export type AgentProof = {
30
+ ok: true;
31
+ publicId: string;
32
+ address: string;
33
+ networkId: string;
34
+ credential?: unknown;
35
+ };
36
+ export type PayAgentProtocolConfig = {
37
+ baseUrl?: string;
38
+ apiKey: string;
39
+ agentId: string;
40
+ networkId: string;
41
+ fetch?: typeof fetch;
42
+ };
43
+ export type LinkPayIntent = {
44
+ type: "link";
45
+ linkUsername: string;
46
+ linkId: string;
47
+ approvalId?: string;
48
+ };
49
+ export type ProfilePayIntent = {
50
+ type: "profile";
51
+ recipientUsername: string;
52
+ amount: string | number;
53
+ tokenId: string;
54
+ networkId: string;
55
+ approvalId?: string;
56
+ };
57
+ export type AddressPayIntent = {
58
+ type: "address";
59
+ recipientAddress: string;
60
+ amount: string | number;
61
+ tokenId: string;
62
+ networkId: string;
63
+ approvalId?: string;
64
+ };
65
+ export type PayIntent = LinkPayIntent | ProfilePayIntent | AddressPayIntent;
66
+ export type PreflightCheck = {
67
+ ok: boolean;
68
+ code?: string;
69
+ message: string;
70
+ };
71
+ export type PreflightResult = {
72
+ ready: boolean;
73
+ type: PayType;
74
+ checks: Record<string, PreflightCheck>;
75
+ };
76
+ export type Destination = {
77
+ type: PayType;
78
+ recipientAddress: string;
79
+ amount: string | number;
80
+ tokenId: string;
81
+ networkId: string;
82
+ linkUsername?: string;
83
+ linkId?: string;
84
+ recipientUsername?: string;
85
+ };
86
+ export type ApprovalRequiredResult = {
87
+ status: "approval_required";
88
+ checks?: Record<string, PreflightCheck>;
89
+ approvalId?: string;
90
+ txHash?: string;
91
+ idempotencyKey?: string;
92
+ };
93
+ export type PaidResult = {
94
+ status: "paid";
95
+ txHash: string;
96
+ idempotencyKey: string;
97
+ destination: Destination;
98
+ record: unknown;
99
+ };
100
+ export type PayResult = ApprovalRequiredResult | PaidResult;
101
+ export type PublicPaymentLink = {
102
+ username: string;
103
+ publicId: string;
104
+ amount: number;
105
+ tokenId: string;
106
+ networkId: string;
107
+ recipientAddress?: string | null;
108
+ canPay?: boolean;
109
+ status?: string;
110
+ };
111
+ export type PublicProfile = {
112
+ username: string;
113
+ wallets: Array<{
114
+ networkId: string;
115
+ address: string;
116
+ }>;
117
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@payagent/wdk-protocol",
3
+ "version": "0.1.0",
4
+ "description": "WDK protocol module for PayAgent. Stateless. Takes a WalletAccount. Never holds keys.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "tsc -p tsconfig.test.json && node --test dist-test/test/*.test.js",
21
+ "staging": "tsc -p tsconfig.json && node --import tsx scripts/staging-usdt-pay.ts",
22
+ "staging:address": "tsc -p tsconfig.json && node --import tsx scripts/staging-usdt-pay.ts address",
23
+ "staging:preflight": "tsc -p tsconfig.json && node --import tsx scripts/staging-usdt-pay.ts preflight",
24
+ "staging:attach": "tsc -p tsconfig.json && node --import tsx scripts/staging-usdt-pay.ts attach",
25
+ "staging:pay": "tsc -p tsconfig.json && node --import tsx scripts/staging-usdt-pay.ts pay --send"
26
+ },
27
+ "dependencies": {
28
+ "@tetherto/wdk": "^1.0.0-beta.17",
29
+ "@tetherto/wdk-wallet-evm": "^1.0.0-beta.17",
30
+ "@tetherto/wdk-wallet-solana": "^1.0.0-beta.15"
31
+ },
32
+ "keywords": [
33
+ "payagent",
34
+ "wdk",
35
+ "tether",
36
+ "usdt"
37
+ ],
38
+ "license": "MIT",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Mrinallcx/fidence.git",
45
+ "directory": "packages/wdk-protocol"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.17.2",
52
+ "tsx": "^4.23.15",
53
+ "typescript": "^5.9.2"
54
+ }
55
+ }