@agentpay-ai/shared 0.1.0 → 0.1.2

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,22 @@
1
+ # @agentpay-ai/shared
2
+
3
+ Shared schemas, chain metadata, token helpers, and intent types for AgentPay.
4
+
5
+ Use this package when building AgentPay-compatible tools that need the same validation rules as the MCP server, setup web, and CLI.
6
+
7
+ ## Included Modules
8
+
9
+ - Approval phrase parsing and payment intent types.
10
+ - X Layer stablecoin metadata and balance helpers.
11
+ - Wallet setup, invoice, x402, x402 Bazaar discovery, account admin, and payment tracking schemas, including x402 `PAYMENT-RESPONSE` and `payment-identifier` proof helpers.
12
+ - Route calldata hashing helpers used by guarded contract calls.
13
+
14
+ ## Example
15
+
16
+ ```ts
17
+ import { preparePaymentInputSchema } from "@agentpay-ai/shared";
18
+
19
+ const input = preparePaymentInputSchema.parse(candidatePayment);
20
+ ```
21
+
22
+ AgentPay validates untrusted inputs at runtime boundaries with Zod. Keep that pattern when extending the package.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@agentpay-ai/shared",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
7
7
  },
8
8
  "files": [
9
+ "README.md",
9
10
  "src/account-admin.ts",
10
11
  "src/approval.ts",
11
12
  "src/balance.ts",
@@ -16,7 +17,8 @@
16
17
  "src/payment-tracking.ts",
17
18
  "src/tokens.ts",
18
19
  "src/wallet-setup.ts",
19
- "src/x402.ts"
20
+ "src/x402.ts",
21
+ "src/x402-bazaar.ts"
20
22
  ],
21
23
  "publishConfig": {
22
24
  "access": "public"
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import { networkSelectionShape } from "./chains.ts";
3
4
  import { evmAddressSchema } from "./payment-intent.ts";
4
5
 
5
6
  const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a positive integer string");
@@ -7,33 +8,40 @@ const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a p
7
8
  export const prepareAccountAdminTransactionInputSchema = z.discriminatedUnion("action", [
8
9
  z.object({
9
10
  action: z.literal("PAUSE"),
11
+ ...networkSelectionShape,
10
12
  }),
11
13
  z.object({
12
14
  action: z.literal("UNPAUSE"),
15
+ ...networkSelectionShape,
13
16
  }),
14
17
  z.object({
15
18
  action: z.literal("SET_EXECUTOR"),
16
19
  newExecutorAddress: evmAddressSchema,
20
+ ...networkSelectionShape,
17
21
  }),
18
22
  z.object({
19
23
  action: z.literal("CANCEL_NONCE"),
20
24
  nonce: positiveIntegerStringSchema,
25
+ ...networkSelectionShape,
21
26
  }),
22
27
  z.object({
23
28
  action: z.literal("SET_ALLOWED_TOKEN"),
24
29
  tokenAddress: evmAddressSchema,
25
30
  allowed: z.boolean(),
31
+ ...networkSelectionShape,
26
32
  }),
27
33
  z.object({
28
34
  action: z.literal("WITHDRAW_NATIVE"),
29
35
  toAddress: evmAddressSchema,
30
36
  amountAtomic: positiveIntegerStringSchema,
37
+ ...networkSelectionShape,
31
38
  }),
32
39
  z.object({
33
40
  action: z.literal("WITHDRAW_TOKEN"),
34
41
  tokenAddress: evmAddressSchema,
35
42
  toAddress: evmAddressSchema,
36
43
  amountAtomic: positiveIntegerStringSchema,
44
+ ...networkSelectionShape,
37
45
  }),
38
46
  ]);
39
47
 
package/src/balance.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { z } from "zod";
2
2
 
3
- import { STABLE_TOKEN_SYMBOLS, stableTokenSymbolSchema } from "./tokens.ts";
3
+ import { networkSelectionShape } from "./chains.ts";
4
+ import { DEFAULT_STABLE_TOKEN_SYMBOLS, stableTokenSymbolSchema } from "./tokens.ts";
4
5
 
5
6
  export const getBalanceInputSchema = z.object({
6
- tokenSymbols: z.array(stableTokenSymbolSchema).min(1).default([...STABLE_TOKEN_SYMBOLS]),
7
+ tokenSymbols: z.array(stableTokenSymbolSchema).min(1).default([...DEFAULT_STABLE_TOKEN_SYMBOLS]),
8
+ ...networkSelectionShape,
7
9
  });
8
10
 
9
11
  export type GetBalanceInput = z.input<typeof getBalanceInputSchema>;
package/src/chains.ts CHANGED
@@ -1,17 +1,19 @@
1
+ import { z } from "zod";
2
+
1
3
  export const SUPPORTED_CHAINS = {
2
- 56: {
3
- id: 56,
4
- name: "BNB Chain",
4
+ 196: {
5
+ id: 196,
6
+ name: "X Layer",
5
7
  nativeCurrency: {
6
- symbol: "BNB",
8
+ symbol: "OKB",
7
9
  decimals: 18,
8
10
  },
9
11
  },
10
- 97: {
11
- id: 97,
12
- name: "BNB Chain Testnet",
12
+ 1952: {
13
+ id: 1952,
14
+ name: "X Layer Testnet",
13
15
  nativeCurrency: {
14
- symbol: "tBNB",
16
+ symbol: "OKB",
15
17
  decimals: 18,
16
18
  },
17
19
  },
@@ -28,6 +30,35 @@ export const SUPPORTED_CHAINS = {
28
30
  export type SupportedChainId = keyof typeof SUPPORTED_CHAINS;
29
31
  export type NativeCurrency = (typeof SUPPORTED_CHAINS)[SupportedChainId]["nativeCurrency"];
30
32
 
33
+ export const X_LAYER_NETWORK_CHAIN_IDS = {
34
+ mainnet: 196,
35
+ testnet: 1952,
36
+ } as const;
37
+
38
+ export const xLayerNetworkSchema = z.enum(["mainnet", "testnet"]);
39
+ export const xLayerHomeChainIdSchema = z.union([z.literal(196), z.literal(1952)]);
40
+ export const networkSelectionShape = {
41
+ network: xLayerNetworkSchema.optional(),
42
+ homeChainId: xLayerHomeChainIdSchema.optional(),
43
+ } as const;
44
+
45
+ export type XLayerNetwork = z.infer<typeof xLayerNetworkSchema>;
46
+ export type XLayerHomeChainId = z.infer<typeof xLayerHomeChainIdSchema>;
47
+ export type NetworkSelectionInput = {
48
+ network?: XLayerNetwork;
49
+ homeChainId?: XLayerHomeChainId;
50
+ };
51
+
52
+ export function resolveXLayerHomeChainId(input: NetworkSelectionInput, fallbackHomeChainId: XLayerHomeChainId = 196): XLayerHomeChainId {
53
+ const networkHomeChainId = input.network ? X_LAYER_NETWORK_CHAIN_IDS[input.network] : undefined;
54
+
55
+ if (networkHomeChainId !== undefined && input.homeChainId !== undefined && networkHomeChainId !== input.homeChainId) {
56
+ throw new Error(`Network ${input.network} maps to chain ${networkHomeChainId}, but homeChainId ${input.homeChainId} was provided.`);
57
+ }
58
+
59
+ return input.homeChainId ?? networkHomeChainId ?? fallbackHomeChainId;
60
+ }
61
+
31
62
  export function getChainName(chainId: number): string {
32
63
  return SUPPORTED_CHAINS[chainId as SupportedChainId]?.name ?? `Chain ${chainId}`;
33
64
  }
package/src/index.ts CHANGED
@@ -8,3 +8,4 @@ export * from "./payment-tracking.ts";
8
8
  export * from "./tokens.ts";
9
9
  export * from "./wallet-setup.ts";
10
10
  export * from "./x402.ts";
11
+ export * from "./x402-bazaar.ts";
package/src/invoice.ts CHANGED
@@ -2,11 +2,11 @@ import { z } from "zod";
2
2
 
3
3
  import { getChainName } from "./chains.ts";
4
4
  import { preparePaymentInputSchema } from "./payment-intent.ts";
5
- import { stableTokenSymbolSchema } from "./tokens.ts";
5
+ import { stableTokenSymbolSchema, type StableTokenSymbol } from "./tokens.ts";
6
6
 
7
7
  export const parseInvoicePaymentInputSchema = z.object({
8
8
  invoice: z.string().trim().min(1),
9
- sourceTokenSymbol: stableTokenSymbolSchema.default("USDT"),
9
+ sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
10
10
  });
11
11
 
12
12
  export type ParseInvoicePaymentInput = z.input<typeof parseInvoicePaymentInputSchema>;
@@ -16,10 +16,10 @@ export interface ParsedInvoicePayment {
16
16
  recipientAddress: string;
17
17
  destinationChainId: number;
18
18
  destinationChain: string;
19
- destinationTokenSymbol: "USDC" | "USDT";
19
+ destinationTokenSymbol: StableTokenSymbol;
20
20
  amountOut: string;
21
21
  purpose: string;
22
- sourceTokenSymbol: "USDC" | "USDT";
22
+ sourceTokenSymbol: StableTokenSymbol;
23
23
  paymentType: "INVOICE_PAYMENT";
24
24
  }
25
25
 
@@ -101,30 +101,30 @@ function parseDestinationChainId(value: string | undefined): number | undefined
101
101
 
102
102
  const normalized = normalizeKey(value);
103
103
  const knownChains: Record<string, number> = {
104
+ xlayer: 196,
105
+ xlayermainnet: 196,
106
+ xlayertestnet: 1952,
104
107
  base: 8453,
105
- bnbchain: 56,
106
- bnb: 56,
107
- bsc: 56,
108
108
  };
109
109
 
110
110
  return knownChains[normalized];
111
111
  }
112
112
 
113
- function parseToken(value: string | undefined): "USDC" | "USDT" | undefined {
113
+ function parseToken(value: string | undefined): StableTokenSymbol | undefined {
114
114
  if (!value) {
115
115
  return undefined;
116
116
  }
117
117
 
118
- return stableTokenSymbolSchema.parse(value.trim().toUpperCase());
118
+ return stableTokenSymbolSchema.parse(normalizeTokenSymbol(value));
119
119
  }
120
120
 
121
121
  function parsePaymentFields(candidate: {
122
122
  recipientAddress: string | undefined;
123
123
  destinationChainId: number | undefined;
124
- destinationTokenSymbol: "USDC" | "USDT" | undefined;
124
+ destinationTokenSymbol: StableTokenSymbol | undefined;
125
125
  amountOut: string | undefined;
126
126
  purpose: string | undefined;
127
- sourceTokenSymbol: "USDC" | "USDT" | undefined;
127
+ sourceTokenSymbol: StableTokenSymbol | undefined;
128
128
  }) {
129
129
  const parsed = preparePaymentInputSchema.safeParse(candidate);
130
130
 
@@ -142,3 +142,7 @@ function invoicePurpose(invoiceId: string | undefined): string | undefined {
142
142
  function normalizeKey(value: string): string {
143
143
  return value.toLowerCase().replace(/[^a-z0-9]/g, "");
144
144
  }
145
+
146
+ function normalizeTokenSymbol(value: string): string {
147
+ return value.trim().toUpperCase().replace("USD₮0", "USDT0");
148
+ }
@@ -2,7 +2,7 @@ import { keccak_256 } from "@noble/hashes/sha3.js";
2
2
  import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
3
3
  import { z } from "zod";
4
4
 
5
- import { getChainName } from "./chains.ts";
5
+ import { getChainName, networkSelectionShape } from "./chains.ts";
6
6
  import { getStableTokenMetadata, stableTokenSymbolSchema } from "./tokens.ts";
7
7
 
8
8
  const addressPattern = /^0x[a-fA-F0-9]{40}$/;
@@ -44,8 +44,9 @@ export const preparePaymentInputSchema = z.object({
44
44
  destinationTokenSymbol: stableTokenSymbolSchema,
45
45
  amountOut: positiveDecimalStringSchema,
46
46
  purpose: z.string().trim().min(1).max(280),
47
- sourceTokenSymbol: stableTokenSymbolSchema.default("USDT"),
47
+ sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
48
48
  paymentType: stablecoinPaymentTypeSchema.default("WALLET_PAYMENT"),
49
+ ...networkSelectionShape,
49
50
  });
50
51
 
51
52
  export const quotePaymentRouteInputSchema = preparePaymentInputSchema.omit({ purpose: true, paymentType: true });
@@ -66,10 +67,11 @@ export type ExecutePaymentInput = z.infer<typeof executePaymentInputSchema>;
66
67
  export const prepareContractCallInputSchema = z.object({
67
68
  targetAddress: evmAddressSchema,
68
69
  callData: hexDataSchema.refine((value) => value !== "0x", "Expected non-empty calldata"),
69
- sourceTokenSymbol: stableTokenSymbolSchema.default("USDT"),
70
+ sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
70
71
  maxTokenSpend: positiveDecimalStringSchema,
71
72
  maxNativeFee: z.string().regex(/^(?:0|[1-9]\d*)$/, "Expected a non-negative integer string").default("0"),
72
73
  purpose: z.string().trim().min(1).max(280),
74
+ ...networkSelectionShape,
73
75
  });
74
76
 
75
77
  export type PrepareContractCallInput = z.input<typeof prepareContractCallInputSchema>;
package/src/tokens.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const STABLE_TOKEN_SYMBOLS = ["USDC", "USDT"] as const;
3
+ export const STABLE_TOKEN_SYMBOLS = ["USDT0", "USDC", "USDT"] as const;
4
+ export const DEFAULT_STABLE_TOKEN_SYMBOLS = ["USDT0", "USDC"] as const satisfies readonly StableTokenSymbol[];
4
5
 
5
6
  export const stableTokenSymbolSchema = z.enum(STABLE_TOKEN_SYMBOLS);
6
7
 
7
8
  export type StableTokenSymbol = z.infer<typeof stableTokenSymbolSchema>;
8
9
 
9
10
  export const STABLE_TOKEN_DECIMALS: Record<StableTokenSymbol, number> = {
11
+ USDT0: 6,
10
12
  USDC: 6,
11
13
  USDT: 6,
12
14
  };
@@ -23,31 +25,25 @@ export type StableTokenMetadataOverrides = Partial<
23
25
 
24
26
  let configuredStableTokenMetadataOverrides: StableTokenMetadataOverrides = {};
25
27
 
26
- export const STABLE_TOKENS_BY_CHAIN: Record<number, Record<StableTokenSymbol, StableTokenMetadata>> = {
27
- 56: {
28
- USDC: {
29
- symbol: "USDC",
30
- address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
31
- decimals: 18,
32
- },
33
- USDT: {
34
- symbol: "USDT",
35
- address: "0x55d398326f99059fF775485246999027B3197955",
36
- decimals: 18,
28
+ export const STABLE_TOKENS_BY_CHAIN: Record<number, Partial<Record<StableTokenSymbol, StableTokenMetadata>>> = {
29
+ 196: {
30
+ USDT0: {
31
+ symbol: "USDT0",
32
+ address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
33
+ decimals: 6,
37
34
  },
38
- },
39
- 97: {
40
35
  USDC: {
41
36
  symbol: "USDC",
42
- address: "0xEC1C60D64a06896Df296438c12edD14E974FDE47",
37
+ address: "0x74b7F16337b8972027F6196A17a631aC6dE26d22",
43
38
  decimals: 6,
44
39
  },
45
40
  USDT: {
46
41
  symbol: "USDT",
47
- address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
48
- decimals: 18,
42
+ address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
43
+ decimals: 6,
49
44
  },
50
45
  },
46
+ 1952: {},
51
47
  8453: {
52
48
  USDC: {
53
49
  symbol: "USDC",
@@ -77,15 +73,18 @@ export function getStableTokenDecimals(symbol: string): number {
77
73
  export function getStableTokenMetadata(chainId: number, symbol: string): StableTokenMetadata {
78
74
  const parsedSymbol = stableTokenSymbolSchema.parse(symbol);
79
75
  const metadata = STABLE_TOKENS_BY_CHAIN[chainId]?.[parsedSymbol];
76
+ const override = configuredStableTokenMetadataOverrides[chainId]?.[parsedSymbol];
80
77
 
81
- if (!metadata) {
78
+ if (!metadata && !override?.address) {
82
79
  throw new Error(`Unsupported stable token ${parsedSymbol} on chain ${chainId}.`);
83
80
  }
84
81
 
85
- const override = configuredStableTokenMetadataOverrides[chainId]?.[parsedSymbol];
86
-
87
82
  return {
88
- ...metadata,
83
+ ...(metadata ?? {
84
+ symbol: parsedSymbol,
85
+ address: override?.address ?? "",
86
+ decimals: STABLE_TOKEN_DECIMALS[parsedSymbol],
87
+ }),
89
88
  ...override,
90
89
  symbol: parsedSymbol,
91
90
  };
@@ -98,3 +97,14 @@ export function getStableTokenAddress(chainId: number, symbol: string): string {
98
97
  export function getStableTokenDecimalsForChain(chainId: number, symbol: string): number {
99
98
  return getStableTokenMetadata(chainId, symbol).decimals;
100
99
  }
100
+
101
+ export function getSupportedStableTokenMetadataForChain(chainId: number): StableTokenMetadata[] {
102
+ const staticTokens = STABLE_TOKENS_BY_CHAIN[chainId] ?? {};
103
+ const overrideTokens = configuredStableTokenMetadataOverrides[chainId] ?? {};
104
+ const symbols = new Set<StableTokenSymbol>([
105
+ ...(Object.keys(staticTokens) as StableTokenSymbol[]),
106
+ ...(Object.keys(overrideTokens) as StableTokenSymbol[]),
107
+ ]);
108
+
109
+ return [...symbols].map((symbol) => getStableTokenMetadata(chainId, symbol));
110
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import { networkSelectionShape } from "./chains.ts";
3
4
  import { evmAddressSchema } from "./payment-intent.ts";
4
5
 
5
6
  export const setupIntentStatusSchema = z.enum(["PENDING", "SIGNED", "DEPLOYING", "COMPLETED", "EXPIRED", "FAILED"]);
@@ -8,6 +9,7 @@ export type SetupIntentStatus = z.infer<typeof setupIntentStatusSchema>;
8
9
 
9
10
  export const prepareWalletCreationInputSchema = z.object({
10
11
  ownerAddress: evmAddressSchema.optional(),
12
+ ...networkSelectionShape,
11
13
  });
12
14
 
13
15
  export type PrepareWalletCreationInput = z.input<typeof prepareWalletCreationInputSchema>;
@@ -18,19 +20,23 @@ export const checkWalletCreationInputSchema = z.object({
18
20
 
19
21
  export type CheckWalletCreationInput = z.infer<typeof checkWalletCreationInputSchema>;
20
22
 
21
- export const getAgentWalletInputSchema = z.object({});
23
+ export const getAgentWalletInputSchema = z.object({
24
+ ...networkSelectionShape,
25
+ });
22
26
 
23
27
  export type GetAgentWalletInput = z.infer<typeof getAgentWalletInputSchema>;
24
28
 
25
29
  export const prepareRouteTargetAllowanceInputSchema = z.object({
26
30
  routeTarget: evmAddressSchema,
27
31
  allowed: z.boolean().default(true),
32
+ ...networkSelectionShape,
28
33
  });
29
34
 
30
35
  export type PrepareRouteTargetAllowanceInput = z.input<typeof prepareRouteTargetAllowanceInputSchema>;
31
36
 
32
37
  export const checkRouteTargetAllowanceInputSchema = z.object({
33
38
  routeTarget: evmAddressSchema,
39
+ ...networkSelectionShape,
34
40
  });
35
41
 
36
42
  export type CheckRouteTargetAllowanceInput = z.infer<typeof checkRouteTargetAllowanceInputSchema>;
@@ -56,4 +62,5 @@ export interface SetupIntentRecord {
56
62
  errorCode?: string;
57
63
  errorMessage?: string;
58
64
  completedAt?: string;
65
+ homeChainId?: number;
59
66
  }
@@ -0,0 +1,273 @@
1
+ import { z } from "zod";
2
+
3
+ const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a positive integer string");
4
+ const httpMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET");
5
+ const bazaarResourceTypeSchema = z.enum(["http", "mcp"]);
6
+ const scalarParameterSchema = z.union([z.string(), z.number(), z.boolean()]);
7
+
8
+ const x402BazaarPaymentRequirementSchema = z
9
+ .object({
10
+ scheme: z.string(),
11
+ network: z.string(),
12
+ amount: positiveIntegerStringSchema,
13
+ asset: z.string(),
14
+ payTo: z.string(),
15
+ maxTimeoutSeconds: z.number().int().positive(),
16
+ extra: z.record(z.string(), z.unknown()).optional(),
17
+ })
18
+ .passthrough();
19
+
20
+ export const x402BazaarResourceSchema = z
21
+ .object({
22
+ resource: z.string().url(),
23
+ type: bazaarResourceTypeSchema,
24
+ x402Version: z.literal(2),
25
+ accepts: z.array(x402BazaarPaymentRequirementSchema).min(1),
26
+ lastUpdated: z.string().optional(),
27
+ description: z.string().optional(),
28
+ mimeType: z.string().optional(),
29
+ serviceName: z.string().optional(),
30
+ tags: z.array(z.string()).optional(),
31
+ iconUrl: z.string().url().optional(),
32
+ extensions: z.record(z.string(), z.unknown()).optional(),
33
+ })
34
+ .passthrough();
35
+
36
+ export const searchX402ServicesInputSchema = z.object({
37
+ query: z.string().trim().min(1),
38
+ type: bazaarResourceTypeSchema.default("http"),
39
+ network: z.string().trim().min(1).optional(),
40
+ limit: z.number().int().min(1).max(20).default(5),
41
+ cursor: z.string().trim().min(1).optional(),
42
+ });
43
+
44
+ export const prepareX402ServiceRequestInputSchema = z.object({
45
+ resource: x402BazaarResourceSchema,
46
+ parameters: z.record(z.string(), scalarParameterSchema).default({}),
47
+ headers: z.record(z.string(), z.string()).default({}),
48
+ body: z.unknown().optional(),
49
+ });
50
+
51
+ export type X402BazaarResource = z.output<typeof x402BazaarResourceSchema>;
52
+ export type X402BazaarPaymentRequirement = z.output<typeof x402BazaarPaymentRequirementSchema>;
53
+ export type SearchX402ServicesInput = z.input<typeof searchX402ServicesInputSchema>;
54
+ export type ParsedSearchX402ServicesInput = z.output<typeof searchX402ServicesInputSchema>;
55
+ export type PrepareX402ServiceRequestInput = z.input<typeof prepareX402ServiceRequestInputSchema>;
56
+ export type ParsedPrepareX402ServiceRequestInput = z.output<typeof prepareX402ServiceRequestInputSchema>;
57
+
58
+ export interface X402BazaarPaymentRequiredObject extends Record<string, unknown> {
59
+ x402Version: 2;
60
+ resource: {
61
+ url: string;
62
+ description?: string;
63
+ serviceName?: string;
64
+ mimeType?: string;
65
+ };
66
+ accepts: X402BazaarPaymentRequirement[];
67
+ extensions?: Record<string, unknown>;
68
+ }
69
+
70
+ export interface BuiltX402BazaarHttpRequest {
71
+ status: "REQUEST_READY" | "NEEDS_INPUT";
72
+ request?: {
73
+ url: string;
74
+ method: z.output<typeof httpMethodSchema>;
75
+ headers: Record<string, string>;
76
+ body?: string;
77
+ };
78
+ paymentRequired?: X402BazaarPaymentRequiredObject;
79
+ missingParameters: string[];
80
+ }
81
+
82
+ export function normalizeX402BazaarResource(rawResource: unknown): X402BazaarResource {
83
+ return x402BazaarResourceSchema.parse(rawResource);
84
+ }
85
+
86
+ export function buildX402BazaarHttpRequest(request: {
87
+ resource: X402BazaarResource;
88
+ parameters: Record<string, string | number | boolean>;
89
+ headers: Record<string, string>;
90
+ body?: unknown;
91
+ }): BuiltX402BazaarHttpRequest {
92
+ if (request.resource.type !== "http") {
93
+ throw new Error("prepare_x402_service_request currently supports Bazaar HTTP resources only.");
94
+ }
95
+
96
+ const input = getBazaarInput(request.resource);
97
+ const method = httpMethodSchema.parse(asString(input.method)?.toUpperCase() ?? "GET");
98
+ const missingParameters = new Set<string>();
99
+ const usedParameters = new Set<string>();
100
+ const routeTemplate = asString(input.routeTemplate) ?? request.resource.resource;
101
+ const templatedPath = applyPathParameters(routeTemplate, request.parameters, usedParameters, missingParameters);
102
+ const url = new URL(resolveRouteTemplate(templatedPath, request.resource.resource));
103
+ const queryParams = asRecord(input.queryParams);
104
+
105
+ for (const key of Object.keys(queryParams ?? {})) {
106
+ usedParameters.add(key);
107
+ if (!Object.hasOwn(request.parameters, key)) {
108
+ missingParameters.add(key);
109
+ } else {
110
+ url.searchParams.set(key, String(request.parameters[key]));
111
+ }
112
+ }
113
+
114
+ for (const key of getJsonSchemaRequiredKeys(input)) {
115
+ if (!Object.hasOwn(request.parameters, key) && request.body === undefined) {
116
+ missingParameters.add(key);
117
+ }
118
+ }
119
+
120
+ if (missingParameters.size > 0) {
121
+ return {
122
+ status: "NEEDS_INPUT",
123
+ missingParameters: [...missingParameters].sort(),
124
+ };
125
+ }
126
+
127
+ const headers = { ...request.headers };
128
+ const body = createRequestBody(method, request.body, request.parameters, usedParameters, headers);
129
+ const requestUrl = url.toString();
130
+
131
+ return {
132
+ status: "REQUEST_READY",
133
+ request: omitUndefined({
134
+ url: requestUrl,
135
+ method,
136
+ headers,
137
+ body,
138
+ }) as BuiltX402BazaarHttpRequest["request"],
139
+ paymentRequired: createPaymentRequiredObject(request.resource, requestUrl),
140
+ missingParameters: [],
141
+ };
142
+ }
143
+
144
+ export function getX402BazaarRequiredParameters(resource: X402BazaarResource): string[] {
145
+ const input = getBazaarInput(resource);
146
+ const routeTemplate = asString(input.routeTemplate) ?? resource.resource;
147
+ return [...new Set([...getPathParameterNames(routeTemplate), ...Object.keys(asRecord(input.queryParams) ?? {}), ...getJsonSchemaRequiredKeys(input)])].sort();
148
+ }
149
+
150
+ export function getX402BazaarHttpMethod(resource: X402BazaarResource): string | undefined {
151
+ const input = getBazaarInput(resource);
152
+ return asString(input.method)?.toUpperCase();
153
+ }
154
+
155
+ function createPaymentRequiredObject(resource: X402BazaarResource, requestUrl: string): X402BazaarPaymentRequiredObject {
156
+ return omitUndefined({
157
+ x402Version: 2,
158
+ resource: omitUndefined({
159
+ url: requestUrl,
160
+ description: resource.description,
161
+ serviceName: resource.serviceName,
162
+ mimeType: resource.mimeType,
163
+ }),
164
+ accepts: resource.accepts,
165
+ extensions: resource.extensions,
166
+ }) as X402BazaarPaymentRequiredObject;
167
+ }
168
+
169
+ function createRequestBody(
170
+ method: z.output<typeof httpMethodSchema>,
171
+ rawBody: unknown,
172
+ parameters: Record<string, string | number | boolean>,
173
+ usedParameters: Set<string>,
174
+ headers: Record<string, string>,
175
+ ): string | undefined {
176
+ if (method === "GET" || method === "DELETE") {
177
+ return undefined;
178
+ }
179
+
180
+ if (rawBody !== undefined) {
181
+ return typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
182
+ }
183
+
184
+ const unusedParameters = Object.fromEntries(
185
+ Object.entries(parameters).filter(([key]) => !usedParameters.has(key)),
186
+ );
187
+
188
+ if (Object.keys(unusedParameters).length === 0) {
189
+ return undefined;
190
+ }
191
+
192
+ if (!hasHeader(headers, "content-type")) {
193
+ headers["Content-Type"] = "application/json";
194
+ }
195
+
196
+ return JSON.stringify(unusedParameters);
197
+ }
198
+
199
+ function applyPathParameters(
200
+ routeTemplate: string,
201
+ parameters: Record<string, string | number | boolean>,
202
+ usedParameters: Set<string>,
203
+ missingParameters: Set<string>,
204
+ ): string {
205
+ return routeTemplate
206
+ .replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, key: string) =>
207
+ replacePathParameter(key, parameters, usedParameters, missingParameters),
208
+ )
209
+ .replace(/\[([A-Za-z_][A-Za-z0-9_]*)\]/g, (_match, key: string) =>
210
+ replacePathParameter(key, parameters, usedParameters, missingParameters),
211
+ )
212
+ .replace(/\/:([A-Za-z_][A-Za-z0-9_]*)/g, (_match, key: string) =>
213
+ `/${replacePathParameter(key, parameters, usedParameters, missingParameters)}`,
214
+ );
215
+ }
216
+
217
+ function replacePathParameter(
218
+ key: string,
219
+ parameters: Record<string, string | number | boolean>,
220
+ usedParameters: Set<string>,
221
+ missingParameters: Set<string>,
222
+ ): string {
223
+ usedParameters.add(key);
224
+ if (!Object.hasOwn(parameters, key)) {
225
+ missingParameters.add(key);
226
+ return `{${key}}`;
227
+ }
228
+
229
+ return encodeURIComponent(String(parameters[key]));
230
+ }
231
+
232
+ function getPathParameterNames(routeTemplate: string): string[] {
233
+ return [
234
+ ...routeTemplate.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g),
235
+ ...routeTemplate.matchAll(/\[([A-Za-z_][A-Za-z0-9_]*)\]/g),
236
+ ...routeTemplate.matchAll(/\/:([A-Za-z_][A-Za-z0-9_]*)/g),
237
+ ].map((match) => match[1]!);
238
+ }
239
+
240
+ function resolveRouteTemplate(routeTemplate: string, resourceUrl: string): string {
241
+ return new URL(routeTemplate, resourceUrl).toString();
242
+ }
243
+
244
+ function getJsonSchemaRequiredKeys(input: Record<string, unknown>): string[] {
245
+ const inputSchema = asRecord(input.inputSchema);
246
+ const required = inputSchema?.required;
247
+ return Array.isArray(required) ? required.filter((key): key is string => typeof key === "string") : [];
248
+ }
249
+
250
+ function getBazaarInput(resource: X402BazaarResource): Record<string, unknown> {
251
+ const extensions = asRecord(resource.extensions);
252
+ const bazaar = asRecord(extensions?.bazaar);
253
+ const info = asRecord(bazaar?.info);
254
+ return asRecord(info?.input) ?? {};
255
+ }
256
+
257
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
258
+ return typeof value === "object" && value !== null && !Array.isArray(value)
259
+ ? (value as Record<string, unknown>)
260
+ : undefined;
261
+ }
262
+
263
+ function asString(value: unknown): string | undefined {
264
+ return typeof value === "string" ? value : undefined;
265
+ }
266
+
267
+ function hasHeader(headers: Record<string, string>, headerName: string): boolean {
268
+ return Object.keys(headers).some((name) => name.toLowerCase() === headerName.toLowerCase());
269
+ }
270
+
271
+ function omitUndefined<T extends Record<string, unknown>>(record: T): Partial<T> {
272
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as Partial<T>;
273
+ }
package/src/x402.ts CHANGED
@@ -1,19 +1,41 @@
1
1
  import { z } from "zod";
2
+ import { keccak_256 } from "@noble/hashes/sha3.js";
3
+ import { bytesToHex } from "@noble/hashes/utils.js";
2
4
 
3
5
  import { getChainName } from "./chains.ts";
4
- import { evmAddressSchema, preparePaymentInputSchema } from "./payment-intent.ts";
6
+ import { evmAddressSchema, preparePaymentInputSchema, type PaymentIntentRecord } from "./payment-intent.ts";
5
7
  import { getStableTokenMetadata, STABLE_TOKEN_SYMBOLS, stableTokenSymbolSchema } from "./tokens.ts";
6
8
  import type { StableTokenSymbol } from "./tokens.ts";
7
9
 
8
10
  const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a positive integer string");
11
+ const PAYMENT_IDENTIFIER = "payment-identifier";
12
+ const paymentIdentifierSchema = z.string().regex(/^[A-Za-z0-9_-]{16,128}$/);
9
13
 
10
14
  export const parseX402PaymentRequiredInputSchema = z.object({
11
15
  paymentRequired: z.union([z.string().trim().min(1), z.record(z.string(), z.unknown())]),
12
- sourceTokenSymbol: stableTokenSymbolSchema.default("USDT"),
16
+ sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
13
17
  });
14
18
 
15
19
  export type ParseX402PaymentRequiredInput = z.input<typeof parseX402PaymentRequiredInputSchema>;
16
20
 
21
+ const retryX402HttpMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET");
22
+
23
+ export const retryX402RequestInputSchema = z.object({
24
+ paymentRequired: parseX402PaymentRequiredInputSchema.shape.paymentRequired,
25
+ paymentIntentId: z.string().trim().min(1),
26
+ request: z
27
+ .object({
28
+ url: z.string().url().optional(),
29
+ method: retryX402HttpMethodSchema,
30
+ headers: z.record(z.string(), z.string()).default({}),
31
+ body: z.string().optional(),
32
+ })
33
+ .default({ method: "GET", headers: {} }),
34
+ });
35
+
36
+ export type RetryX402RequestInput = z.input<typeof retryX402RequestInputSchema>;
37
+ export type ParsedRetryX402RequestInput = z.output<typeof retryX402RequestInputSchema>;
38
+
17
39
  const x402ResourceInfoSchema = z
18
40
  .object({
19
41
  url: z.string().url(),
@@ -75,13 +97,51 @@ export interface ParsedX402PaymentRequired {
75
97
  sourceTokenSymbol: StableTokenSymbol;
76
98
  paymentType: "X402_PAYMENT";
77
99
  };
100
+ extensions?: {
101
+ "payment-identifier"?: {
102
+ info: {
103
+ required: boolean;
104
+ };
105
+ };
106
+ };
78
107
  standardX402SignatureRequired: true;
79
108
  }
80
109
 
110
+ export interface AgentPayX402PaymentProof {
111
+ x402Version: 2;
112
+ scheme: "agentpay-receipt";
113
+ paymentIntentId: string;
114
+ paymentType: "X402_PAYMENT";
115
+ payer: string;
116
+ ownerAddress: string;
117
+ resourceUrl: string;
118
+ requirementHash: string;
119
+ network: string;
120
+ chainId: number;
121
+ asset: string;
122
+ payTo: string;
123
+ amount: string;
124
+ amountDecimal: string;
125
+ sourceTxHash: string;
126
+ destinationTxHash?: string;
127
+ settlementTxHash: string;
128
+ completedAt?: string;
129
+ paymentIdentifier?: string;
130
+ extensions?: {
131
+ "payment-identifier"?: {
132
+ info: {
133
+ required: boolean;
134
+ id: string;
135
+ };
136
+ };
137
+ };
138
+ }
139
+
81
140
  export function parseX402PaymentRequired(rawInput: ParseX402PaymentRequiredInput): ParsedX402PaymentRequired {
82
141
  const input = parseX402PaymentRequiredInputSchema.parse(rawInput);
83
142
  const paymentRequired = x402PaymentRequiredSchema.parse(decodePaymentRequired(input.paymentRequired));
84
143
  const selected = paymentRequired.accepts.map(toSupportedRequirement).find((requirement) => requirement !== null);
144
+ const paymentIdentifierExtension = parsePaymentIdentifierExtension(paymentRequired.extensions);
85
145
 
86
146
  if (!selected) {
87
147
  throw new Error("No AgentPay-supported x402 payment requirement was found.");
@@ -117,10 +177,84 @@ export function parseX402PaymentRequired(rawInput: ParseX402PaymentRequiredInput
117
177
  sourceTokenSymbol: paymentInput.sourceTokenSymbol,
118
178
  paymentType: "X402_PAYMENT",
119
179
  },
180
+ ...(paymentIdentifierExtension
181
+ ? {
182
+ extensions: {
183
+ [PAYMENT_IDENTIFIER]: paymentIdentifierExtension,
184
+ },
185
+ }
186
+ : {}),
120
187
  standardX402SignatureRequired: true,
121
188
  };
122
189
  }
123
190
 
191
+ export function createAgentPayX402PaymentHeader(request: {
192
+ parsed: ParsedX402PaymentRequired;
193
+ paymentIntent: PaymentIntentRecord;
194
+ }): string {
195
+ return encodeBase64UrlJson(createAgentPayX402PaymentProof(request));
196
+ }
197
+
198
+ export function decodeAgentPayX402PaymentHeader(header: string): AgentPayX402PaymentProof {
199
+ const decoded = JSON.parse(Buffer.from(header, "base64url").toString("utf8")) as unknown;
200
+ return agentPayX402PaymentProofSchema.parse(decoded);
201
+ }
202
+
203
+ export function createAgentPayX402PaymentProof(request: {
204
+ parsed: ParsedX402PaymentRequired;
205
+ paymentIntent: PaymentIntentRecord;
206
+ }): AgentPayX402PaymentProof {
207
+ const { parsed, paymentIntent } = request;
208
+ const selected = parsed.selectedRequirement;
209
+
210
+ validateCompletedX402Intent(parsed, paymentIntent);
211
+
212
+ const settlementTxHash = paymentIntent.destinationTxHash ?? paymentIntent.sourceTxHash;
213
+ const paymentIdentifierExtension = parsed.extensions?.[PAYMENT_IDENTIFIER];
214
+ const paymentIdentifier = paymentIdentifierExtension ? createPaymentIdentifier(paymentIntent.id) : undefined;
215
+
216
+ return {
217
+ x402Version: 2,
218
+ scheme: "agentpay-receipt",
219
+ paymentIntentId: paymentIntent.id,
220
+ paymentType: "X402_PAYMENT",
221
+ payer: paymentIntent.accountAddress,
222
+ ownerAddress: paymentIntent.ownerAddress,
223
+ resourceUrl: parsed.resource.url,
224
+ requirementHash: createX402RequirementHash(parsed),
225
+ network: selected.network,
226
+ chainId: selected.chainId,
227
+ asset: selected.asset,
228
+ payTo: selected.payTo,
229
+ amount: selected.amountAtomic,
230
+ amountDecimal: selected.amount,
231
+ sourceTxHash: paymentIntent.sourceTxHash!,
232
+ ...(paymentIntent.destinationTxHash ? { destinationTxHash: paymentIntent.destinationTxHash } : {}),
233
+ settlementTxHash: settlementTxHash!,
234
+ ...(paymentIntent.completedAt ? { completedAt: paymentIntent.completedAt } : {}),
235
+ ...(paymentIdentifier
236
+ ? {
237
+ paymentIdentifier,
238
+ extensions: {
239
+ [PAYMENT_IDENTIFIER]: {
240
+ info: {
241
+ required: paymentIdentifierExtension!.info.required,
242
+ id: paymentIdentifier,
243
+ },
244
+ },
245
+ },
246
+ }
247
+ : {}),
248
+ };
249
+ }
250
+
251
+ export function createX402RequirementHash(parsed: ParsedX402PaymentRequired): string {
252
+ return `0x${bytesToHex(keccak_256(new TextEncoder().encode(stableStringify({
253
+ resourceUrl: parsed.resource.url,
254
+ selectedRequirement: parsed.selectedRequirement,
255
+ }))))}`;
256
+ }
257
+
124
258
  function decodePaymentRequired(paymentRequired: string | Record<string, unknown>): unknown {
125
259
  if (typeof paymentRequired !== "string") {
126
260
  return paymentRequired;
@@ -132,6 +266,104 @@ function decodePaymentRequired(paymentRequired: string | Record<string, unknown>
132
266
  return JSON.parse(json) as unknown;
133
267
  }
134
268
 
269
+ const agentPayX402PaymentProofSchema = z.object({
270
+ x402Version: z.literal(2),
271
+ scheme: z.literal("agentpay-receipt"),
272
+ paymentIntentId: z.string().min(1),
273
+ paymentType: z.literal("X402_PAYMENT"),
274
+ payer: evmAddressSchema,
275
+ ownerAddress: evmAddressSchema,
276
+ resourceUrl: z.string().url(),
277
+ requirementHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
278
+ network: z.string().min(1),
279
+ chainId: z.number().int().positive(),
280
+ asset: z.string().min(1),
281
+ payTo: evmAddressSchema,
282
+ amount: positiveIntegerStringSchema,
283
+ amountDecimal: z.string().min(1),
284
+ sourceTxHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
285
+ destinationTxHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/).optional(),
286
+ settlementTxHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
287
+ completedAt: z.string().optional(),
288
+ paymentIdentifier: paymentIdentifierSchema.optional(),
289
+ extensions: z
290
+ .object({
291
+ "payment-identifier": z
292
+ .object({
293
+ info: z.object({
294
+ required: z.boolean(),
295
+ id: paymentIdentifierSchema,
296
+ }),
297
+ })
298
+ .optional(),
299
+ })
300
+ .optional(),
301
+ });
302
+
303
+ function validateCompletedX402Intent(parsed: ParsedX402PaymentRequired, paymentIntent: PaymentIntentRecord): void {
304
+ if (paymentIntent.status !== "COMPLETED") {
305
+ throw new Error(`Payment intent ${paymentIntent.id} must be COMPLETED before x402 proof can be generated.`);
306
+ }
307
+
308
+ if (paymentIntent.paymentType !== "X402_PAYMENT") {
309
+ throw new Error(`Payment intent ${paymentIntent.id} must be an X402_PAYMENT intent.`);
310
+ }
311
+
312
+ if (!paymentIntent.sourceTxHash) {
313
+ throw new Error(`Payment intent ${paymentIntent.id} is missing a source transaction hash.`);
314
+ }
315
+
316
+ const selected = parsed.selectedRequirement;
317
+ const matchesRequirement =
318
+ paymentIntent.destinationChainId === selected.chainId &&
319
+ paymentIntent.destinationTokenAddress.toLowerCase() === selected.asset.toLowerCase() &&
320
+ paymentIntent.destinationTokenSymbol === selected.tokenSymbol &&
321
+ paymentIntent.recipientAddress.toLowerCase() === selected.payTo.toLowerCase() &&
322
+ paymentIntent.amountOut === selected.amount;
323
+
324
+ if (!matchesRequirement) {
325
+ throw new Error(`Payment intent ${paymentIntent.id} does not match the x402 requirement.`);
326
+ }
327
+ }
328
+
329
+ function encodeBase64UrlJson(value: unknown): string {
330
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
331
+ }
332
+
333
+ function parsePaymentIdentifierExtension(
334
+ extensions: Record<string, unknown> | undefined,
335
+ ): ParsedX402PaymentRequired["extensions"] extends infer Extensions
336
+ ? Extensions extends { "payment-identifier"?: infer Extension }
337
+ ? Extension | undefined
338
+ : never
339
+ : never {
340
+ const extension = extensions?.[PAYMENT_IDENTIFIER];
341
+
342
+ if (extension === undefined) {
343
+ return undefined;
344
+ }
345
+
346
+ if (!extension || typeof extension !== "object") {
347
+ throw new Error("Unsupported x402 payment-identifier extension.");
348
+ }
349
+
350
+ const info = (extension as { info?: unknown }).info;
351
+
352
+ if (!info || typeof info !== "object" || typeof (info as { required?: unknown }).required !== "boolean") {
353
+ throw new Error("Unsupported x402 payment-identifier extension.");
354
+ }
355
+
356
+ return {
357
+ info: {
358
+ required: (info as { required: boolean }).required,
359
+ },
360
+ };
361
+ }
362
+
363
+ function createPaymentIdentifier(paymentIntentId: string): string {
364
+ return paymentIdentifierSchema.parse(paymentIntentId);
365
+ }
366
+
135
367
  function toSupportedRequirement(requirement: z.infer<typeof x402PaymentRequirementSchema>):
136
368
  | ParsedX402PaymentRequired["selectedRequirement"]
137
369
  | null {
@@ -202,3 +434,19 @@ function createX402Purpose(resource: z.infer<typeof x402ResourceInfoSchema>): st
202
434
 
203
435
  return `x402 payment for ${details}`.slice(0, 280);
204
436
  }
437
+
438
+ function stableStringify(value: unknown): string {
439
+ if (Array.isArray(value)) {
440
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
441
+ }
442
+
443
+ if (value && typeof value === "object") {
444
+ return `{${Object.entries(value as Record<string, unknown>)
445
+ .filter(([, entryValue]) => entryValue !== undefined)
446
+ .sort(([left], [right]) => left.localeCompare(right))
447
+ .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)
448
+ .join(",")}}`;
449
+ }
450
+
451
+ return JSON.stringify(value);
452
+ }