@4mica/x402 0.1.0 → 1.0.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 CHANGED
@@ -277,6 +277,20 @@ const client = new x402Client()
277
277
  const fetchWithPayment = wrapFetchWithPayment(fetch, client);
278
278
  ```
279
279
 
280
+ ## V2 Validation Policy Notes
281
+
282
+ For x402 V2 requests, include validation policy metadata in `paymentRequirements.extra`:
283
+
284
+ - `validationRegistryAddress`
285
+ - `validatorAddress`
286
+ - `validatorAgentId`
287
+ - `minValidationScore`
288
+ - optional `requiredValidationTag`
289
+
290
+ `validationChainId` is optional. When omitted, the underlying 4mica SDK derives the
291
+ `validation_chain_id` from the CAIP-2 `network` value (for example, `eip155:1`).
292
+ If `validationChainId` is provided, it must match that network chain id.
293
+
280
294
  ## Complete Example
281
295
 
282
296
  ### Server
package/demo/README.md CHANGED
@@ -32,7 +32,7 @@ cp .env.example .env
32
32
 
33
33
  Required variables:
34
34
  - `PRIVATE_KEY`: Your Ethereum private key (with 0x prefix) for Sepolia testnet
35
- - `PAY_TO_ADDRESS`: Address that will receive payments (optional, defaults to a test address)
35
+ - `PAY_TO_ADDRESS`: Address that will receive payments
36
36
 
37
37
  ## Running the Demo
38
38
 
@@ -54,7 +54,7 @@ You should see:
54
54
  ```
55
55
  x402 Demo Server running on http://localhost:3000
56
56
  Protected endpoint: http://localhost:3000/api/premium-data
57
- Payment required: $0.05 (4mica credit on Sepolia)
57
+ Payment required: $0.01 (4mica credit on Sepolia)
58
58
  ```
59
59
 
60
60
  ### Terminal 2: Run the client
@@ -82,7 +82,7 @@ PRIVATE_KEY=0xYourPrivateKey yarn client
82
82
  ## What Happens
83
83
 
84
84
  1. **Server** starts with one protected endpoint: `GET /api/premium-data`
85
- - Requires a payment of $0.05 in 4mica credits on Sepolia
85
+ - Requires a payment of $0.01 in 4mica credits on Sepolia
86
86
  - Uses x402 payment protocol
87
87
 
88
88
  2. **Client** makes a request to the protected endpoint:
package/demo/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "dev": "tsx watch src/server.ts"
11
11
  },
12
12
  "dependencies": {
13
- "@4mica/sdk": "file:../../../../../ts-sdk-4mica",
13
+ "@4mica/sdk": "^0.5.3",
14
14
  "@4mica/x402": "file:..",
15
15
  "@x402/fetch": "^2.2.0",
16
16
  "dotenv": "^16.4.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4mica/x402",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript x402 utilities for interacting with the 4Mica payment network",
6
6
  "license": "MIT",
@@ -5,6 +5,7 @@ import {
5
5
  PaymentRequirementsV1,
6
6
  X402Flow,
7
7
  X402PaymentRequired,
8
+ X402ResourceInfo,
8
9
  } from '@4mica/sdk'
9
10
  import { Account } from 'viem/accounts'
10
11
  import { SUPPORTED_NETWORKS } from '../server/scheme.js'
@@ -73,9 +74,23 @@ export class FourMicaEvmScheme implements SchemeNetworkClient {
73
74
  payload: signed.payload as unknown as Record<string, unknown>,
74
75
  }
75
76
  } else if (x402Version === 2) {
77
+ const resourcePayload =
78
+ paymentRequirements.extra &&
79
+ typeof paymentRequirements.extra === 'object' &&
80
+ 'resource' in paymentRequirements.extra &&
81
+ typeof paymentRequirements.extra.resource === 'object' &&
82
+ paymentRequirements.extra.resource !== null
83
+ ? (paymentRequirements.extra.resource as Record<string, unknown>)
84
+ : {}
85
+
86
+ const resource: X402ResourceInfo = {
87
+ url: String(resourcePayload.url ?? ''),
88
+ description: String(resourcePayload.description ?? ''),
89
+ mimeType: String(resourcePayload.mimeType ?? ''),
90
+ }
76
91
  const paymentRequired: X402PaymentRequired = {
77
92
  x402Version: 2,
78
- resource: { url: '', description: '', mimeType: '' },
93
+ resource,
79
94
  accepts: [paymentRequirements],
80
95
  }
81
96
  const signed = await x402Flow.signPaymentV2(
@@ -1,5 +1,5 @@
1
1
  import { FacilitatorConfig, HTTPFacilitatorClient } from '@x402/core/server'
2
- import { Network, PaymentRequirements } from '@x402/core/types'
2
+ import { Network, PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types'
3
3
 
4
4
  const DEFAULT_FACILITATOR_URL = 'https://x402.4mica.xyz'
5
5
 
@@ -21,6 +21,18 @@ export interface OpenTabResponse {
21
21
  nextReqId: string
22
22
  }
23
23
 
24
+ export interface CertificateResponse {
25
+ claims: string
26
+ signature: string
27
+ }
28
+
29
+ export type FourMicaSettleResponse = SettleResponse & {
30
+ certificate?: CertificateResponse
31
+ txHash?: string
32
+ networkId?: string
33
+ error?: string
34
+ }
35
+
24
36
  export class OpenTabError extends Error {
25
37
  constructor(
26
38
  public readonly status: number,
@@ -75,6 +87,41 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
75
87
  throw new Error(`Facilitator openTab failed (${response.status}): ${JSON.stringify(data)}`)
76
88
  }
77
89
 
90
+ async settle(
91
+ paymentPayload: PaymentPayload,
92
+ paymentRequirements: PaymentRequirements
93
+ ): Promise<FourMicaSettleResponse> {
94
+ let headers: Record<string, string> = {
95
+ 'Content-Type': 'application/json',
96
+ }
97
+
98
+ const authHeaders = await this.createAuthHeaders('settle')
99
+ headers = { ...headers, ...authHeaders.headers }
100
+
101
+ const response = await fetch(`${this.url}/settle`, {
102
+ method: 'POST',
103
+ headers,
104
+ body: JSON.stringify(
105
+ this.safeJson({
106
+ x402Version: paymentPayload.x402Version,
107
+ paymentPayload,
108
+ paymentRequirements,
109
+ })
110
+ ),
111
+ })
112
+
113
+ const data = (await response.json()) as Record<string, unknown>
114
+ const normalized = normalizeSettleResponse(data, paymentRequirements)
115
+
116
+ if (!response.ok || !normalized.success) {
117
+ throw new Error(
118
+ `Facilitator settle failed (${response.status}): ${normalized.errorReason ?? normalized.error ?? 'unknown error'}`
119
+ )
120
+ }
121
+
122
+ return normalized
123
+ }
124
+
78
125
  /**
79
126
  * Helper to convert objects to JSON-safe format.
80
127
  * Handles BigInt and other non-JSON types.
@@ -88,3 +135,70 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
88
135
  )
89
136
  }
90
137
  }
138
+
139
+ function normalizeSettleResponse(
140
+ payload: Record<string, unknown>,
141
+ requirements: PaymentRequirements
142
+ ): FourMicaSettleResponse {
143
+ const transaction = String(
144
+ payload.transaction ??
145
+ payload.transactionHash ??
146
+ payload.txHash ??
147
+ payload.tx_hash ??
148
+ payload.hash ??
149
+ ''
150
+ )
151
+ const network = String(
152
+ payload.network ?? payload.networkId ?? payload.network_id ?? requirements.network
153
+ ) as Network
154
+ const errorReason =
155
+ typeof payload.errorReason === 'string'
156
+ ? payload.errorReason
157
+ : typeof payload.error_reason === 'string'
158
+ ? payload.error_reason
159
+ : typeof payload.error === 'string'
160
+ ? payload.error
161
+ : typeof payload.message === 'string'
162
+ ? payload.message
163
+ : undefined
164
+
165
+ const certificate =
166
+ payload.certificate &&
167
+ typeof payload.certificate === 'object' &&
168
+ typeof (payload.certificate as Record<string, unknown>).claims === 'string' &&
169
+ typeof (payload.certificate as Record<string, unknown>).signature === 'string'
170
+ ? {
171
+ claims: (payload.certificate as Record<string, string>).claims,
172
+ signature: (payload.certificate as Record<string, string>).signature,
173
+ }
174
+ : undefined
175
+
176
+ return {
177
+ success: Boolean(payload.success ?? errorReason === undefined),
178
+ errorReason,
179
+ payer:
180
+ typeof payload.payer === 'string'
181
+ ? payload.payer
182
+ : typeof payload.userAddress === 'string'
183
+ ? payload.userAddress
184
+ : typeof payload.user_address === 'string'
185
+ ? payload.user_address
186
+ : undefined,
187
+ transaction,
188
+ network,
189
+ certificate,
190
+ txHash:
191
+ typeof payload.txHash === 'string'
192
+ ? payload.txHash
193
+ : typeof payload.tx_hash === 'string'
194
+ ? payload.tx_hash
195
+ : transaction || undefined,
196
+ networkId:
197
+ typeof payload.networkId === 'string'
198
+ ? payload.networkId
199
+ : typeof payload.network_id === 'string'
200
+ ? payload.network_id
201
+ : network,
202
+ error: typeof payload.error === 'string' ? payload.error : undefined,
203
+ }
204
+ }
@@ -0,0 +1,101 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { privateKeyToAccount } from 'viem/accounts'
3
+
4
+ import { FourMicaEvmScheme } from '../src/client/scheme.js'
5
+
6
+ describe('FourMicaEvmScheme', () => {
7
+ afterEach(() => {
8
+ vi.restoreAllMocks()
9
+ })
10
+
11
+ it('passes V2 resource metadata through to the SDK flow', async () => {
12
+ const signPaymentV2 = vi.fn().mockResolvedValue({
13
+ payload: {
14
+ claims: {
15
+ version: 'v2',
16
+ validation_request_hash: '0x' + '11'.repeat(32),
17
+ validation_subject_hash: '0x' + '22'.repeat(32),
18
+ },
19
+ },
20
+ })
21
+
22
+ vi.spyOn(FourMicaEvmScheme as never, 'createX402Flow' as never).mockResolvedValue({
23
+ signPayment: vi.fn(),
24
+ signPaymentV2,
25
+ } as never)
26
+
27
+ const scheme = await FourMicaEvmScheme.create(
28
+ privateKeyToAccount(`0x${'11'.repeat(32)}`)
29
+ )
30
+
31
+ const requirements = {
32
+ scheme: '4mica-credit',
33
+ network: 'eip155:11155111',
34
+ asset: '0x2222222222222222222222222222222222222222',
35
+ amount: '10',
36
+ payTo: '0x1111111111111111111111111111111111111111',
37
+ extra: {
38
+ rpcUrl: 'https://custom.rpc.example',
39
+ validationRegistryAddress: '0x3333333333333333333333333333333333333333',
40
+ validatorAddress: '0x4444444444444444444444444444444444444444',
41
+ validatorAgentId: '7',
42
+ minValidationScore: 80,
43
+ requiredValidationTag: 'hard-finality',
44
+ resource: {
45
+ url: 'https://api.example.com/premium',
46
+ description: 'Premium dataset',
47
+ mimeType: 'application/json',
48
+ },
49
+ },
50
+ }
51
+
52
+ const result = await scheme.createPaymentPayload(2, requirements as never)
53
+
54
+ expect(result.x402Version).toBe(2)
55
+ expect(signPaymentV2).toHaveBeenCalledTimes(1)
56
+
57
+ const paymentRequired = signPaymentV2.mock.calls[0]?.[0]
58
+ expect(paymentRequired.resource).toEqual({
59
+ url: 'https://api.example.com/premium',
60
+ description: 'Premium dataset',
61
+ mimeType: 'application/json',
62
+ })
63
+
64
+ const accepted = signPaymentV2.mock.calls[0]?.[1]
65
+ expect(accepted).toMatchObject({
66
+ scheme: '4mica-credit',
67
+ network: 'eip155:11155111',
68
+ asset: '0x2222222222222222222222222222222222222222',
69
+ amount: '10',
70
+ payTo: '0x1111111111111111111111111111111111111111',
71
+ })
72
+ expect(accepted.extra).toMatchObject({
73
+ validationRegistryAddress: '0x3333333333333333333333333333333333333333',
74
+ validatorAddress: '0x4444444444444444444444444444444444444444',
75
+ validatorAgentId: '7',
76
+ minValidationScore: 80,
77
+ requiredValidationTag: 'hard-finality',
78
+ })
79
+ })
80
+
81
+ it('rejects unsupported x402 versions', async () => {
82
+ vi.spyOn(FourMicaEvmScheme as never, 'createX402Flow' as never).mockResolvedValue({
83
+ signPayment: vi.fn(),
84
+ signPaymentV2: vi.fn(),
85
+ } as never)
86
+
87
+ const scheme = await FourMicaEvmScheme.create(
88
+ privateKeyToAccount(`0x${'11'.repeat(32)}`)
89
+ )
90
+
91
+ await expect(
92
+ scheme.createPaymentPayload(3, {
93
+ scheme: '4mica-credit',
94
+ network: 'eip155:11155111',
95
+ asset: '0x2222222222222222222222222222222222222222',
96
+ amount: '10',
97
+ payTo: '0x1111111111111111111111111111111111111111',
98
+ } as never)
99
+ ).rejects.toThrow('Unsupported x402Version: 3')
100
+ })
101
+ })
@@ -0,0 +1,174 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { FourMicaFacilitatorClient } from '../src/server/facilitator.js'
4
+
5
+ describe('FourMicaFacilitatorClient', () => {
6
+ afterEach(() => {
7
+ vi.restoreAllMocks()
8
+ vi.unstubAllGlobals()
9
+ })
10
+
11
+ it('normalizes 4mica settle responses and preserves certificate fields', async () => {
12
+ const fetchMock = vi.fn().mockResolvedValue(
13
+ new Response(
14
+ JSON.stringify({
15
+ success: true,
16
+ txHash: '0xdeadbeef',
17
+ networkId: 'eip155:11155111',
18
+ certificate: {
19
+ claims: '0x' + '11'.repeat(32),
20
+ signature: '0x' + '22'.repeat(96),
21
+ },
22
+ }),
23
+ {
24
+ status: 200,
25
+ headers: { 'content-type': 'application/json' },
26
+ }
27
+ )
28
+ )
29
+
30
+ vi.stubGlobal('fetch', fetchMock)
31
+
32
+ const client = new FourMicaFacilitatorClient({ url: 'https://facilitator.example' })
33
+
34
+ const result = await client.settle(
35
+ {
36
+ x402Version: 2,
37
+ accepted: {
38
+ scheme: '4mica-credit',
39
+ network: 'eip155:11155111',
40
+ asset: '0x2222222222222222222222222222222222222222',
41
+ amount: '10',
42
+ payTo: '0x1111111111111111111111111111111111111111',
43
+ },
44
+ payload: {
45
+ claims: {
46
+ version: 'v2',
47
+ },
48
+ signature: '0x1234',
49
+ scheme: 'eip712',
50
+ },
51
+ } as never,
52
+ {
53
+ scheme: '4mica-credit',
54
+ network: 'eip155:11155111',
55
+ asset: '0x2222222222222222222222222222222222222222',
56
+ amount: '10',
57
+ payTo: '0x1111111111111111111111111111111111111111',
58
+ } as never
59
+ )
60
+
61
+ expect(result.success).toBe(true)
62
+ expect(result.transaction).toBe('0xdeadbeef')
63
+ expect(result.network).toBe('eip155:11155111')
64
+ expect(result.txHash).toBe('0xdeadbeef')
65
+ expect(result.networkId).toBe('eip155:11155111')
66
+ expect(result.certificate).toEqual({
67
+ claims: '0x' + '11'.repeat(32),
68
+ signature: '0x' + '22'.repeat(96),
69
+ })
70
+
71
+ const request = fetchMock.mock.calls[0]?.[1]
72
+ expect(typeof request?.body).toBe('string')
73
+ expect(String(request?.body)).toContain('"x402Version":2')
74
+ expect(String(request?.body)).toContain('"version":"v2"')
75
+ })
76
+
77
+ it('normalizes alias fields when the facilitator omits txHash/networkId', async () => {
78
+ const fetchMock = vi.fn().mockResolvedValue(
79
+ new Response(
80
+ JSON.stringify({
81
+ success: true,
82
+ transactionHash: '0xabc123',
83
+ network: 'eip155:80002',
84
+ user_address: '0x9999999999999999999999999999999999999999',
85
+ }),
86
+ {
87
+ status: 200,
88
+ headers: { 'content-type': 'application/json' },
89
+ }
90
+ )
91
+ )
92
+
93
+ vi.stubGlobal('fetch', fetchMock)
94
+
95
+ const client = new FourMicaFacilitatorClient({ url: 'https://facilitator.example' })
96
+ const result = await client.settle(
97
+ {
98
+ x402Version: 2,
99
+ accepted: {
100
+ scheme: '4mica-credit',
101
+ network: 'eip155:80002',
102
+ asset: '0x2222222222222222222222222222222222222222',
103
+ amount: '10',
104
+ payTo: '0x1111111111111111111111111111111111111111',
105
+ },
106
+ payload: {
107
+ claims: { version: 'v2' },
108
+ signature: '0x1234',
109
+ scheme: 'eip712',
110
+ },
111
+ } as never,
112
+ {
113
+ scheme: '4mica-credit',
114
+ network: 'eip155:80002',
115
+ asset: '0x2222222222222222222222222222222222222222',
116
+ amount: '10',
117
+ payTo: '0x1111111111111111111111111111111111111111',
118
+ } as never
119
+ )
120
+
121
+ expect(result.success).toBe(true)
122
+ expect(result.transaction).toBe('0xabc123')
123
+ expect(result.txHash).toBe('0xabc123')
124
+ expect(result.network).toBe('eip155:80002')
125
+ expect(result.networkId).toBe('eip155:80002')
126
+ expect(result.payer).toBe('0x9999999999999999999999999999999999999999')
127
+ })
128
+
129
+ it('surfaces facilitator errors using normalized reasons', async () => {
130
+ const fetchMock = vi.fn().mockResolvedValue(
131
+ new Response(
132
+ JSON.stringify({
133
+ success: false,
134
+ error_reason: 'unsupported x402Version 2',
135
+ }),
136
+ {
137
+ status: 200,
138
+ headers: { 'content-type': 'application/json' },
139
+ }
140
+ )
141
+ )
142
+
143
+ vi.stubGlobal('fetch', fetchMock)
144
+
145
+ const client = new FourMicaFacilitatorClient({ url: 'https://facilitator.example' })
146
+
147
+ await expect(
148
+ client.settle(
149
+ {
150
+ x402Version: 2,
151
+ accepted: {
152
+ scheme: '4mica-credit',
153
+ network: 'eip155:11155111',
154
+ asset: '0x2222222222222222222222222222222222222222',
155
+ amount: '10',
156
+ payTo: '0x1111111111111111111111111111111111111111',
157
+ },
158
+ payload: {
159
+ claims: { version: 'v2' },
160
+ signature: '0x1234',
161
+ scheme: 'eip712',
162
+ },
163
+ } as never,
164
+ {
165
+ scheme: '4mica-credit',
166
+ network: 'eip155:11155111',
167
+ asset: '0x2222222222222222222222222222222222222222',
168
+ amount: '10',
169
+ payTo: '0x1111111111111111111111111111111111111111',
170
+ } as never
171
+ )
172
+ ).rejects.toThrow('unsupported x402Version 2')
173
+ })
174
+ })