@ucptools/validator 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.
Files changed (121) hide show
  1. package/CLAUDE.md +109 -0
  2. package/CONTRIBUTING.md +113 -0
  3. package/LICENSE +21 -0
  4. package/README.md +203 -0
  5. package/api/analyze-feed.js +140 -0
  6. package/api/badge.js +185 -0
  7. package/api/benchmark.js +177 -0
  8. package/api/directory-stats.ts +29 -0
  9. package/api/directory.ts +73 -0
  10. package/api/generate-compliance.js +143 -0
  11. package/api/generate-schema.js +457 -0
  12. package/api/generate.js +132 -0
  13. package/api/security-scan.js +133 -0
  14. package/api/simulate.js +187 -0
  15. package/api/tsconfig.json +10 -0
  16. package/api/validate.js +1351 -0
  17. package/apify-actor/.actor/actor.json +68 -0
  18. package/apify-actor/.actor/input_schema.json +32 -0
  19. package/apify-actor/APIFY-STORE-LISTING.md +412 -0
  20. package/apify-actor/Dockerfile +8 -0
  21. package/apify-actor/README.md +166 -0
  22. package/apify-actor/main.ts +111 -0
  23. package/apify-actor/package.json +17 -0
  24. package/apify-actor/src/main.js +199 -0
  25. package/docs/BRAND-IDENTITY.md +238 -0
  26. package/docs/BRAND-STYLE-GUIDE.md +356 -0
  27. package/drizzle/0000_black_king_cobra.sql +39 -0
  28. package/drizzle/meta/0000_snapshot.json +309 -0
  29. package/drizzle/meta/_journal.json +13 -0
  30. package/drizzle.config.ts +10 -0
  31. package/examples/full-profile.json +70 -0
  32. package/examples/minimal-profile.json +23 -0
  33. package/package.json +69 -0
  34. package/public/.well-known/ucp +25 -0
  35. package/public/android-chrome-192x192.png +0 -0
  36. package/public/android-chrome-512x512.png +0 -0
  37. package/public/apple-touch-icon.png +0 -0
  38. package/public/brand.css +321 -0
  39. package/public/directory.html +701 -0
  40. package/public/favicon-16x16.png +0 -0
  41. package/public/favicon-32x32.png +0 -0
  42. package/public/favicon.ico +0 -0
  43. package/public/guides/bigcommerce.html +743 -0
  44. package/public/guides/fastucp.html +838 -0
  45. package/public/guides/magento.html +779 -0
  46. package/public/guides/shopify.html +726 -0
  47. package/public/guides/squarespace.html +749 -0
  48. package/public/guides/wix.html +747 -0
  49. package/public/guides/woocommerce.html +733 -0
  50. package/public/index.html +3835 -0
  51. package/public/learn.html +396 -0
  52. package/public/logo.jpeg +0 -0
  53. package/public/og-image-icon.png +0 -0
  54. package/public/og-image.png +0 -0
  55. package/public/robots.txt +6 -0
  56. package/public/site.webmanifest +31 -0
  57. package/public/sitemap.xml +69 -0
  58. package/public/social/linkedin-banner-1128x191.png +0 -0
  59. package/public/social/temp.PNG +0 -0
  60. package/public/social/x-header-1500x500.png +0 -0
  61. package/public/verify.html +410 -0
  62. package/scripts/generate-favicons.js +44 -0
  63. package/scripts/generate-ico.js +23 -0
  64. package/scripts/generate-og-image.js +45 -0
  65. package/scripts/reset-db.ts +77 -0
  66. package/scripts/seed-db.ts +71 -0
  67. package/scripts/setup-benchmark-db.js +70 -0
  68. package/src/api/server.ts +266 -0
  69. package/src/cli/index.ts +302 -0
  70. package/src/compliance/compliance-generator.ts +452 -0
  71. package/src/compliance/index.ts +28 -0
  72. package/src/compliance/templates.ts +338 -0
  73. package/src/compliance/types.ts +170 -0
  74. package/src/db/index.ts +28 -0
  75. package/src/db/schema.ts +84 -0
  76. package/src/feed-analyzer/feed-analyzer.ts +726 -0
  77. package/src/feed-analyzer/index.ts +34 -0
  78. package/src/feed-analyzer/types.ts +354 -0
  79. package/src/generator/index.ts +7 -0
  80. package/src/generator/key-generator.ts +124 -0
  81. package/src/generator/profile-builder.ts +402 -0
  82. package/src/hosting/artifacts-generator.ts +679 -0
  83. package/src/hosting/index.ts +6 -0
  84. package/src/index.ts +105 -0
  85. package/src/security/index.ts +15 -0
  86. package/src/security/security-scanner.ts +604 -0
  87. package/src/security/types.ts +55 -0
  88. package/src/services/directory.ts +434 -0
  89. package/src/simulator/agent-simulator.ts +941 -0
  90. package/src/simulator/index.ts +7 -0
  91. package/src/simulator/types.ts +170 -0
  92. package/src/types/generator.ts +140 -0
  93. package/src/types/index.ts +7 -0
  94. package/src/types/ucp-profile.ts +140 -0
  95. package/src/types/validation.ts +89 -0
  96. package/src/validator/index.ts +194 -0
  97. package/src/validator/network-validator.ts +417 -0
  98. package/src/validator/rules-validator.ts +297 -0
  99. package/src/validator/sdk-validator.ts +330 -0
  100. package/src/validator/structural-validator.ts +476 -0
  101. package/tests/fixtures/non-compliant-profile.json +25 -0
  102. package/tests/fixtures/official-sample-profile.json +75 -0
  103. package/tests/integration/benchmark.test.ts +207 -0
  104. package/tests/integration/database.test.ts +163 -0
  105. package/tests/integration/directory-api.test.ts +268 -0
  106. package/tests/integration/simulate-api.test.ts +230 -0
  107. package/tests/integration/validate-api.test.ts +269 -0
  108. package/tests/setup.ts +15 -0
  109. package/tests/unit/agent-simulator.test.ts +575 -0
  110. package/tests/unit/compliance-generator.test.ts +374 -0
  111. package/tests/unit/directory-service.test.ts +272 -0
  112. package/tests/unit/feed-analyzer.test.ts +517 -0
  113. package/tests/unit/lint-suggestions.test.ts +423 -0
  114. package/tests/unit/official-samples.test.ts +211 -0
  115. package/tests/unit/pdf-report.test.ts +390 -0
  116. package/tests/unit/sdk-validator.test.ts +531 -0
  117. package/tests/unit/security-scanner.test.ts +410 -0
  118. package/tests/unit/validation.test.ts +390 -0
  119. package/tsconfig.json +20 -0
  120. package/vercel.json +34 -0
  121. package/vitest.config.ts +22 -0
@@ -0,0 +1,701 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <!-- Google Analytics -->
5
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-J5JSHV7H1E"></script>
6
+ <script>
7
+ window.dataLayer = window.dataLayer || [];
8
+ function gtag(){dataLayer.push(arguments);}
9
+ gtag('js', new Date());
10
+ gtag('config', 'G-J5JSHV7H1E');
11
+ </script>
12
+ <!-- Ahrefs Analytics -->
13
+ <script src="https://analytics.ahrefs.com/analytics.js" data-key="w/KDij6w81HzA8oXaw60JA" async></script>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>UCP Merchant Directory - AI-Ready E-commerce Stores | Universal Commerce Protocol</title>
17
+ <meta name="description" content="Browse the public directory of Universal Commerce Protocol (UCP) enabled merchants. Discover AI-ready e-commerce stores compatible with Google AI Mode, ChatGPT Shopping, and Microsoft Copilot checkout.">
18
+ <meta name="keywords" content="UCP Directory, AI-Ready Stores, Universal Commerce Protocol Merchants, AI Commerce Directory, UCP Enabled Stores, AI Shopping Compatible">
19
+ <meta name="robots" content="index, follow">
20
+ <link rel="canonical" href="https://ucptools.dev/directory">
21
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
22
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
23
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
24
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
25
+ <meta name="theme-color" content="#2E86AB">
26
+
27
+ <!-- Open Graph / Facebook -->
28
+ <meta property="og:type" content="website">
29
+ <meta property="og:url" content="https://ucptools.dev/directory">
30
+ <meta property="og:title" content="UCP Merchant Directory - AI-Ready E-commerce Stores">
31
+ <meta property="og:description" content="Browse Universal Commerce Protocol (UCP) enabled merchants ready for AI shopping agents.">
32
+ <meta property="og:image" content="https://ucptools.dev/og-image.png">
33
+ <meta property="og:site_name" content="UCP.tools">
34
+
35
+ <!-- Twitter Card -->
36
+ <meta name="twitter:card" content="summary_large_image">
37
+ <meta name="twitter:url" content="https://ucptools.dev/directory">
38
+ <meta name="twitter:title" content="UCP Merchant Directory - AI-Ready E-commerce Stores">
39
+ <meta name="twitter:description" content="Discover UCP-enabled e-commerce stores compatible with AI shopping agents.">
40
+ <meta name="twitter:image" content="https://ucptools.dev/og-image.png">
41
+
42
+ <!-- JSON-LD Structured Data -->
43
+ <script type="application/ld+json">
44
+ {
45
+ "@context": "https://schema.org",
46
+ "@type": "CollectionPage",
47
+ "name": "UCP Merchant Directory",
48
+ "description": "Directory of Universal Commerce Protocol (UCP) enabled merchants ready for AI commerce.",
49
+ "url": "https://ucptools.dev/directory",
50
+ "isPartOf": {
51
+ "@type": "WebSite",
52
+ "name": "UCP.tools",
53
+ "url": "https://ucptools.dev/"
54
+ },
55
+ "breadcrumb": {
56
+ "@type": "BreadcrumbList",
57
+ "itemListElement": [
58
+ {"@type": "ListItem", "position": 1, "name": "Home", "item": "https://ucptools.dev/"},
59
+ {"@type": "ListItem", "position": 2, "name": "Merchant Directory", "item": "https://ucptools.dev/directory"}
60
+ ]
61
+ }
62
+ }
63
+ </script>
64
+ <style>
65
+ :root {
66
+ --brand-blue: #2E86AB;
67
+ --brand-teal: #36B5A2;
68
+ --brand-green: #47C97A;
69
+ --brand-gradient: linear-gradient(135deg, #2E86AB 0%, #36B5A2 50%, #47C97A 100%);
70
+ --brand-gradient-hover: linear-gradient(135deg, #267593 0%, #2EA18F 50%, #3BB86B 100%);
71
+ --color-dark: #1A2B3C;
72
+ --color-medium: #5A6978;
73
+ --color-light: #94A3B8;
74
+ --color-border: #E2E8F0;
75
+ --color-background: #F8FAFC;
76
+ --color-card: #FFFFFF;
77
+ --color-success: #47C97A;
78
+ --color-warning: #F59E0B;
79
+ --color-error: #EF4444;
80
+ --color-info: #2E86AB;
81
+ --grade-a-bg: #DCFCE7; --grade-a-text: #16A34A;
82
+ --grade-b-bg: #DBEAFE; --grade-b-text: #2563EB;
83
+ --grade-c-bg: #FEF9C3; --grade-c-text: #CA8A04;
84
+ --grade-d-bg: #FED7AA; --grade-d-text: #EA580C;
85
+ --grade-f-bg: #FEE2E2; --grade-f-text: #DC2626;
86
+ }
87
+
88
+ * { box-sizing: border-box; margin: 0; padding: 0; }
89
+ body {
90
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
91
+ background: var(--color-background);
92
+ color: var(--color-dark);
93
+ line-height: 1.6;
94
+ }
95
+ .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
96
+
97
+ /* Header */
98
+ header {
99
+ background: var(--color-card);
100
+ border-bottom: 1px solid var(--color-border);
101
+ padding: 16px 0;
102
+ position: sticky;
103
+ top: 0;
104
+ z-index: 1000;
105
+ }
106
+ .header-inner { display: flex; justify-content: space-between; align-items: center; }
107
+ .logo { display: flex; align-items: center; gap: 12px; font-size: 24px; font-weight: 700; text-decoration: none; }
108
+ .logo-icon { width: 40px; height: 40px; border-radius: 8px; }
109
+ .logo-text { background: var(--brand-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
110
+ .logo-suffix { font-weight: 400; -webkit-text-fill-color: var(--color-medium); }
111
+ nav a { margin-left: 24px; color: var(--color-medium); text-decoration: none; transition: color 0.2s; }
112
+ nav a:hover, nav a.active { color: var(--brand-blue); }
113
+ /* Dropdown */
114
+ .dropdown { position: relative; display: inline-block; margin-left: 24px; }
115
+ .dropdown-toggle { color: var(--color-medium); cursor: pointer; background: none; border: none; font-size: inherit; font-family: inherit; padding: 0; transition: color 0.2s; }
116
+ .dropdown-toggle:hover, .dropdown:hover .dropdown-toggle { color: var(--brand-blue); }
117
+ .dropdown-toggle::after { content: '▼'; font-size: 0.6em; margin-left: 4px; vertical-align: middle; }
118
+ .dropdown-menu { display: none; position: absolute; top: 100%; left: 0; background: var(--color-card); border: 1px solid var(--color-border); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 180px; z-index: 1001; padding: 8px 0; margin-top: 8px; }
119
+ .dropdown:hover .dropdown-menu { display: block; }
120
+ .dropdown-menu a { display: block; padding: 10px 16px; color: var(--color-medium); text-decoration: none; margin-left: 0; }
121
+ .dropdown-menu a:hover { background: rgba(46,134,171,0.08); color: var(--brand-blue); }
122
+ .dropdown-divider { height: 1px; background: var(--color-border); margin: 8px 0; }
123
+
124
+ /* Hero */
125
+ .hero {
126
+ text-align: center;
127
+ padding: 48px 20px;
128
+ background: linear-gradient(135deg, rgba(46, 134, 171, 0.1) 0%, rgba(54, 181, 162, 0.05) 50%, rgba(71, 201, 122, 0.1) 100%);
129
+ }
130
+ .hero h1 { font-size: 36px; margin-bottom: 12px; }
131
+ .hero h1 .gradient-text { background: var(--brand-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
132
+ .hero p { font-size: 18px; color: var(--color-medium); max-width: 600px; margin: 0 auto 24px; }
133
+
134
+ /* Stats Bar */
135
+ .stats-bar {
136
+ display: flex;
137
+ justify-content: center;
138
+ gap: 32px;
139
+ flex-wrap: wrap;
140
+ }
141
+ .stat-item { text-align: center; }
142
+ .stat-value { font-size: 28px; font-weight: 700; background: var(--brand-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
143
+ .stat-label { font-size: 14px; color: var(--color-medium); }
144
+
145
+ /* Main Content */
146
+ .main-content { padding: 40px 0; }
147
+ .content-grid { display: grid; grid-template-columns: 280px 1fr; gap: 32px; }
148
+ @media (max-width: 900px) { .content-grid { grid-template-columns: 1fr; } }
149
+
150
+ /* Sidebar */
151
+ .sidebar { display: flex; flex-direction: column; gap: 24px; }
152
+ .filter-card {
153
+ background: var(--color-card);
154
+ border: 1px solid var(--color-border);
155
+ border-radius: 12px;
156
+ padding: 20px;
157
+ }
158
+ .filter-card h3 { font-size: 14px; font-weight: 600; color: var(--color-medium); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
159
+ .search-input {
160
+ width: 100%;
161
+ padding: 12px 16px;
162
+ border: 2px solid var(--color-border);
163
+ border-radius: 8px;
164
+ font-size: 14px;
165
+ transition: border-color 0.2s;
166
+ }
167
+ .search-input:focus { outline: none; border-color: var(--brand-blue); }
168
+ .filter-list { list-style: none; }
169
+ .filter-item {
170
+ display: flex;
171
+ align-items: center;
172
+ padding: 8px 0;
173
+ cursor: pointer;
174
+ transition: color 0.2s;
175
+ }
176
+ .filter-item:hover { color: var(--brand-blue); }
177
+ .filter-item input { margin-right: 10px; }
178
+ .filter-count { margin-left: auto; font-size: 12px; color: var(--color-light); background: var(--color-background); padding: 2px 8px; border-radius: 10px; }
179
+
180
+ /* Submit CTA */
181
+ .submit-cta {
182
+ background: var(--brand-gradient);
183
+ border-radius: 12px;
184
+ padding: 20px;
185
+ color: white;
186
+ text-align: center;
187
+ }
188
+ .submit-cta h3 { font-size: 16px; margin-bottom: 8px; }
189
+ .submit-cta p { font-size: 14px; opacity: 0.9; margin-bottom: 16px; }
190
+ .submit-btn {
191
+ display: inline-block;
192
+ padding: 10px 20px;
193
+ background: white;
194
+ color: var(--brand-blue);
195
+ border-radius: 6px;
196
+ font-weight: 600;
197
+ text-decoration: none;
198
+ transition: transform 0.2s;
199
+ }
200
+ .submit-btn:hover { transform: translateY(-2px); }
201
+
202
+ /* Merchant Grid */
203
+ .merchant-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
204
+ .merchant-card {
205
+ background: var(--color-card);
206
+ border: 1px solid var(--color-border);
207
+ border-radius: 12px;
208
+ padding: 20px;
209
+ transition: all 0.2s;
210
+ text-decoration: none;
211
+ color: inherit;
212
+ display: block;
213
+ }
214
+ .merchant-card:hover { border-color: var(--brand-blue); box-shadow: 0 4px 12px rgba(46, 134, 171, 0.15); }
215
+ .merchant-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
216
+ .merchant-logo {
217
+ width: 48px;
218
+ height: 48px;
219
+ border-radius: 8px;
220
+ background: var(--color-background);
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ font-size: 20px;
225
+ font-weight: 700;
226
+ color: var(--brand-blue);
227
+ }
228
+ .merchant-logo img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; }
229
+ .merchant-info { flex: 1; min-width: 0; }
230
+ .merchant-name { font-weight: 600; font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
231
+ .merchant-domain { font-size: 13px; color: var(--color-medium); }
232
+ .merchant-grade {
233
+ padding: 4px 10px;
234
+ border-radius: 6px;
235
+ font-weight: 700;
236
+ font-size: 14px;
237
+ }
238
+ .grade-A { background: var(--grade-a-bg); color: var(--grade-a-text); }
239
+ .grade-B { background: var(--grade-b-bg); color: var(--grade-b-text); }
240
+ .grade-C { background: var(--grade-c-bg); color: var(--grade-c-text); }
241
+ .grade-D { background: var(--grade-d-bg); color: var(--grade-d-text); }
242
+ .grade-F { background: var(--grade-f-bg); color: var(--grade-f-text); }
243
+ .merchant-desc { font-size: 14px; color: var(--color-medium); margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
244
+ .merchant-tags { display: flex; flex-wrap: wrap; gap: 6px; }
245
+ .merchant-tag { font-size: 11px; padding: 3px 8px; background: var(--color-background); border-radius: 4px; color: var(--color-medium); }
246
+ .transport-tag { background: rgba(46, 134, 171, 0.1); color: var(--brand-blue); }
247
+
248
+ /* Pagination */
249
+ .pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 32px; }
250
+ .page-btn {
251
+ padding: 8px 16px;
252
+ border: 1px solid var(--color-border);
253
+ background: var(--color-card);
254
+ border-radius: 6px;
255
+ cursor: pointer;
256
+ transition: all 0.2s;
257
+ }
258
+ .page-btn:hover:not(:disabled) { border-color: var(--brand-blue); color: var(--brand-blue); }
259
+ .page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
260
+ .page-info { color: var(--color-medium); font-size: 14px; }
261
+
262
+ /* Loading & Empty States */
263
+ .loading, .empty-state { text-align: center; padding: 60px 20px; color: var(--color-medium); }
264
+ .loading::after { content: ''; display: inline-block; width: 24px; height: 24px; border: 3px solid var(--color-border); border-top-color: var(--brand-blue); border-radius: 50%; animation: spin 1s linear infinite; margin-left: 12px; vertical-align: middle; }
265
+ @keyframes spin { to { transform: rotate(360deg); } }
266
+ .empty-state h3 { font-size: 18px; color: var(--color-dark); margin-bottom: 8px; }
267
+
268
+ /* Modal */
269
+ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
270
+ .modal-overlay.active { display: flex; }
271
+ .modal {
272
+ background: var(--color-card);
273
+ border-radius: 16px;
274
+ padding: 32px;
275
+ max-width: 500px;
276
+ width: 90%;
277
+ max-height: 90vh;
278
+ overflow-y: auto;
279
+ }
280
+ .modal h2 { font-size: 24px; margin-bottom: 8px; }
281
+ .modal p.subtitle { color: var(--color-medium); margin-bottom: 24px; }
282
+ .form-group { margin-bottom: 16px; }
283
+ .form-group label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; }
284
+ .form-group input, .form-group select, .form-group textarea {
285
+ width: 100%;
286
+ padding: 12px;
287
+ border: 2px solid var(--color-border);
288
+ border-radius: 8px;
289
+ font-size: 14px;
290
+ }
291
+ .form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--brand-blue); }
292
+ .form-actions { display: flex; gap: 12px; margin-top: 24px; }
293
+ .btn { padding: 12px 24px; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.2s; border: none; }
294
+ .btn-primary { background: var(--brand-gradient); color: white; }
295
+ .btn-primary:hover { background: var(--brand-gradient-hover); }
296
+ .btn-secondary { background: var(--color-background); color: var(--color-dark); }
297
+ .btn-secondary:hover { background: var(--color-border); }
298
+ .form-message { padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
299
+ .form-message.error { background: var(--grade-f-bg); color: var(--grade-f-text); }
300
+ .form-message.success { background: var(--grade-a-bg); color: var(--grade-a-text); }
301
+
302
+ /* Footer */
303
+ footer { text-align: center; padding: 40px 20px; color: var(--color-medium); font-size: 14px; border-top: 1px solid var(--color-border); margin-top: 60px; }
304
+ footer a { color: var(--brand-blue); text-decoration: none; }
305
+ </style>
306
+ </head>
307
+ <body>
308
+ <header>
309
+ <div class="container header-inner">
310
+ <a href="/" class="logo">
311
+ <img src="/logo.jpeg" alt="UCP.tools" class="logo-icon">
312
+ <span class="logo-text">UCP<span class="logo-suffix">.tools</span></span>
313
+ </a>
314
+ <nav>
315
+ <a href="/">Validator</a>
316
+ <a href="/learn">Learn</a>
317
+ <a href="/directory" class="active">Directory</a>
318
+ <div class="dropdown">
319
+ <span class="dropdown-toggle">Guides</span>
320
+ <div class="dropdown-menu">
321
+ <a href="/guides/fastucp">⚡ FastUCP</a>
322
+ <div class="dropdown-divider"></div>
323
+ <a href="/guides/shopify">🛒 Shopify</a>
324
+ <a href="/guides/woocommerce">🔌 WooCommerce</a>
325
+ <a href="/guides/magento">🧱 Magento</a>
326
+ <a href="/guides/wix">✨ Wix</a>
327
+ <a href="/guides/bigcommerce">📦 BigCommerce</a>
328
+ <a href="/guides/squarespace">◼️ Squarespace</a>
329
+ </div>
330
+ </div>
331
+ </nav>
332
+ </div>
333
+ </header>
334
+
335
+ <section class="hero">
336
+ <div class="container">
337
+ <h1><span class="gradient-text">UCP Merchant</span> Directory</h1>
338
+ <p>Discover stores ready for AI commerce. Browse verified UCP-enabled merchants compatible with AI shopping agents.</p>
339
+ <div class="stats-bar" id="stats-bar">
340
+ <div class="stat-item">
341
+ <div class="stat-value" id="stat-merchants">-</div>
342
+ <div class="stat-label">Merchants</div>
343
+ </div>
344
+ <div class="stat-item">
345
+ <div class="stat-value" id="stat-categories">-</div>
346
+ <div class="stat-label">Categories</div>
347
+ </div>
348
+ <div class="stat-item">
349
+ <div class="stat-value" id="stat-countries">-</div>
350
+ <div class="stat-label">Countries</div>
351
+ </div>
352
+ <div class="stat-item">
353
+ <div class="stat-value" id="stat-avg-score">-</div>
354
+ <div class="stat-label">Avg Score</div>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </section>
359
+
360
+ <main class="main-content">
361
+ <div class="container">
362
+ <div class="content-grid">
363
+ <aside class="sidebar">
364
+ <div class="filter-card">
365
+ <h3>Search</h3>
366
+ <input type="text" class="search-input" id="search-input" placeholder="Search merchants...">
367
+ </div>
368
+
369
+ <div class="filter-card">
370
+ <h3>Categories</h3>
371
+ <ul class="filter-list" id="category-filters">
372
+ <li class="filter-item"><span>Loading...</span></li>
373
+ </ul>
374
+ </div>
375
+
376
+ <div class="filter-card">
377
+ <h3>Grade</h3>
378
+ <ul class="filter-list" id="grade-filters">
379
+ <li class="filter-item"><label><input type="checkbox" value="A"> Grade A</label></li>
380
+ <li class="filter-item"><label><input type="checkbox" value="B"> Grade B</label></li>
381
+ <li class="filter-item"><label><input type="checkbox" value="C"> Grade C</label></li>
382
+ <li class="filter-item"><label><input type="checkbox" value="D"> Grade D</label></li>
383
+ </ul>
384
+ </div>
385
+
386
+ <div class="submit-cta">
387
+ <h3>List Your Store</h3>
388
+ <p>Get discovered by AI agents and join the directory.</p>
389
+ <a href="#" class="submit-btn" id="open-submit-modal">Submit Store</a>
390
+ </div>
391
+ </aside>
392
+
393
+ <section class="merchants-section">
394
+ <div class="merchant-grid" id="merchant-grid">
395
+ <div class="loading">Loading merchants</div>
396
+ </div>
397
+
398
+ <div class="pagination" id="pagination" style="display: none;">
399
+ <button class="page-btn" id="prev-page" disabled>&larr; Previous</button>
400
+ <span class="page-info" id="page-info">Page 1 of 1</span>
401
+ <button class="page-btn" id="next-page" disabled>Next &rarr;</button>
402
+ </div>
403
+ </section>
404
+ </div>
405
+ </div>
406
+ </main>
407
+
408
+ <!-- Submit Modal -->
409
+ <div class="modal-overlay" id="submit-modal">
410
+ <div class="modal">
411
+ <h2>Submit Your Store</h2>
412
+ <p class="subtitle">Add your UCP-enabled store to the directory.</p>
413
+
414
+ <div class="form-message" id="form-message" style="display: none;"></div>
415
+
416
+ <form id="submit-form">
417
+ <div class="form-group">
418
+ <label for="submit-domain">Domain *</label>
419
+ <input type="text" id="submit-domain" placeholder="example.com" required>
420
+ </div>
421
+ <div class="form-group">
422
+ <label for="submit-name">Display Name</label>
423
+ <input type="text" id="submit-name" placeholder="Your Store Name">
424
+ </div>
425
+ <div class="form-group">
426
+ <label for="submit-description">Description</label>
427
+ <textarea id="submit-description" rows="3" placeholder="Brief description of your store..."></textarea>
428
+ </div>
429
+ <div class="form-group">
430
+ <label for="submit-category">Category</label>
431
+ <select id="submit-category">
432
+ <option value="">Select a category</option>
433
+ <option value="fashion">Fashion & Apparel</option>
434
+ <option value="electronics">Electronics</option>
435
+ <option value="home">Home & Garden</option>
436
+ <option value="beauty">Beauty & Health</option>
437
+ <option value="food">Food & Grocery</option>
438
+ <option value="sports">Sports & Outdoors</option>
439
+ <option value="toys">Toys & Games</option>
440
+ <option value="books">Books & Media</option>
441
+ <option value="automotive">Automotive</option>
442
+ <option value="other">Other</option>
443
+ </select>
444
+ </div>
445
+ <div class="form-group">
446
+ <label for="submit-country">Country</label>
447
+ <select id="submit-country">
448
+ <option value="">Select a country</option>
449
+ <option value="US">United States</option>
450
+ <option value="GB">United Kingdom</option>
451
+ <option value="DE">Germany</option>
452
+ <option value="FR">France</option>
453
+ <option value="CA">Canada</option>
454
+ <option value="AU">Australia</option>
455
+ <option value="NL">Netherlands</option>
456
+ <option value="ES">Spain</option>
457
+ <option value="IT">Italy</option>
458
+ <option value="JP">Japan</option>
459
+ </select>
460
+ </div>
461
+ <div class="form-actions">
462
+ <button type="button" class="btn btn-secondary" id="close-modal">Cancel</button>
463
+ <button type="submit" class="btn btn-primary" id="submit-btn">Submit Store</button>
464
+ </div>
465
+ </form>
466
+ </div>
467
+ </div>
468
+
469
+ <footer>
470
+ <div class="container">
471
+ <p>&copy; 2025 UCP.tools - <a href="https://ucp.dev" target="_blank">Universal Commerce Protocol</a></p>
472
+ </div>
473
+ </footer>
474
+
475
+ <script>
476
+ const API_BASE = '/api';
477
+ let currentPage = 1;
478
+ let totalPages = 1;
479
+ let currentFilters = { search: '', category: '', country: '' };
480
+
481
+ // Load stats
482
+ async function loadStats() {
483
+ try {
484
+ const res = await fetch(`${API_BASE}/directory-stats`);
485
+ const data = await res.json();
486
+
487
+ document.getElementById('stat-merchants').textContent = data.stats.totalMerchants;
488
+ document.getElementById('stat-categories').textContent = data.stats.totalCategories;
489
+ document.getElementById('stat-countries').textContent = data.stats.totalCountries;
490
+ document.getElementById('stat-avg-score').textContent = data.stats.avgScore;
491
+
492
+ // Update category filters
493
+ if (data.topCategories.length > 0) {
494
+ const categoryList = document.getElementById('category-filters');
495
+ categoryList.innerHTML = data.topCategories.map(cat => `
496
+ <li class="filter-item">
497
+ <label>
498
+ <input type="checkbox" value="${cat.name}">
499
+ ${formatCategory(cat.name)}
500
+ </label>
501
+ <span class="filter-count">${cat.count}</span>
502
+ </li>
503
+ `).join('');
504
+
505
+ // Add event listeners
506
+ categoryList.querySelectorAll('input').forEach(input => {
507
+ input.addEventListener('change', () => {
508
+ currentFilters.category = input.checked ? input.value : '';
509
+ currentPage = 1;
510
+ loadMerchants();
511
+ });
512
+ });
513
+ }
514
+ } catch (error) {
515
+ console.error('Failed to load stats:', error);
516
+ }
517
+ }
518
+
519
+ // Load merchants
520
+ async function loadMerchants() {
521
+ const grid = document.getElementById('merchant-grid');
522
+ grid.innerHTML = '<div class="loading">Loading merchants</div>';
523
+
524
+ try {
525
+ const params = new URLSearchParams({
526
+ page: currentPage,
527
+ limit: 12,
528
+ ...(currentFilters.search && { search: currentFilters.search }),
529
+ ...(currentFilters.category && { category: currentFilters.category }),
530
+ });
531
+
532
+ const res = await fetch(`${API_BASE}/directory?${params}`);
533
+ const data = await res.json();
534
+
535
+ totalPages = data.pagination.totalPages;
536
+
537
+ if (data.merchants.length === 0) {
538
+ grid.innerHTML = `
539
+ <div class="empty-state">
540
+ <h3>No merchants found</h3>
541
+ <p>Try adjusting your filters or be the first to submit your store!</p>
542
+ </div>
543
+ `;
544
+ } else {
545
+ grid.innerHTML = data.merchants.map(renderMerchantCard).join('');
546
+ }
547
+
548
+ updatePagination(data.pagination);
549
+ } catch (error) {
550
+ console.error('Failed to load merchants:', error);
551
+ grid.innerHTML = '<div class="empty-state"><h3>Error loading merchants</h3><p>Please try again later.</p></div>';
552
+ }
553
+ }
554
+
555
+ function renderMerchantCard(merchant) {
556
+ const initials = (merchant.displayName || merchant.domain).substring(0, 2).toUpperCase();
557
+ const transports = merchant.transports || [];
558
+
559
+ return `
560
+ <a href="https://${merchant.domain}" target="_blank" class="merchant-card">
561
+ <div class="merchant-header">
562
+ <div class="merchant-logo">
563
+ ${merchant.logoUrl ? `<img src="${merchant.logoUrl}" alt="${merchant.displayName}">` : initials}
564
+ </div>
565
+ <div class="merchant-info">
566
+ <div class="merchant-name">${merchant.displayName}</div>
567
+ <div class="merchant-domain">${merchant.domain}</div>
568
+ </div>
569
+ ${merchant.ucpGrade ? `<div class="merchant-grade grade-${merchant.ucpGrade}">${merchant.ucpGrade}</div>` : ''}
570
+ </div>
571
+ ${merchant.description ? `<div class="merchant-desc">${merchant.description}</div>` : ''}
572
+ <div class="merchant-tags">
573
+ ${merchant.category ? `<span class="merchant-tag">${formatCategory(merchant.category)}</span>` : ''}
574
+ ${merchant.countryCode ? `<span class="merchant-tag">${merchant.countryCode}</span>` : ''}
575
+ ${transports.map(t => `<span class="merchant-tag transport-tag">${t}</span>`).join('')}
576
+ </div>
577
+ </a>
578
+ `;
579
+ }
580
+
581
+ function formatCategory(cat) {
582
+ if (!cat) return '';
583
+ return cat.charAt(0).toUpperCase() + cat.slice(1).replace(/-/g, ' ');
584
+ }
585
+
586
+ function updatePagination(pagination) {
587
+ const paginationEl = document.getElementById('pagination');
588
+ const pageInfo = document.getElementById('page-info');
589
+ const prevBtn = document.getElementById('prev-page');
590
+ const nextBtn = document.getElementById('next-page');
591
+
592
+ if (pagination.totalPages <= 1) {
593
+ paginationEl.style.display = 'none';
594
+ return;
595
+ }
596
+
597
+ paginationEl.style.display = 'flex';
598
+ pageInfo.textContent = `Page ${pagination.page} of ${pagination.totalPages}`;
599
+ prevBtn.disabled = pagination.page <= 1;
600
+ nextBtn.disabled = pagination.page >= pagination.totalPages;
601
+ }
602
+
603
+ // Search handler
604
+ let searchTimeout;
605
+ document.getElementById('search-input').addEventListener('input', (e) => {
606
+ clearTimeout(searchTimeout);
607
+ searchTimeout = setTimeout(() => {
608
+ currentFilters.search = e.target.value;
609
+ currentPage = 1;
610
+ loadMerchants();
611
+ }, 300);
612
+ });
613
+
614
+ // Pagination handlers
615
+ document.getElementById('prev-page').addEventListener('click', () => {
616
+ if (currentPage > 1) {
617
+ currentPage--;
618
+ loadMerchants();
619
+ }
620
+ });
621
+
622
+ document.getElementById('next-page').addEventListener('click', () => {
623
+ if (currentPage < totalPages) {
624
+ currentPage++;
625
+ loadMerchants();
626
+ }
627
+ });
628
+
629
+ // Modal handlers
630
+ const modal = document.getElementById('submit-modal');
631
+ document.getElementById('open-submit-modal').addEventListener('click', (e) => {
632
+ e.preventDefault();
633
+ modal.classList.add('active');
634
+ });
635
+
636
+ document.getElementById('close-modal').addEventListener('click', () => {
637
+ modal.classList.remove('active');
638
+ });
639
+
640
+ modal.addEventListener('click', (e) => {
641
+ if (e.target === modal) modal.classList.remove('active');
642
+ });
643
+
644
+ // Submit form handler
645
+ document.getElementById('submit-form').addEventListener('submit', async (e) => {
646
+ e.preventDefault();
647
+
648
+ const formMessage = document.getElementById('form-message');
649
+ const submitBtn = document.getElementById('submit-btn');
650
+
651
+ formMessage.style.display = 'none';
652
+ submitBtn.disabled = true;
653
+ submitBtn.textContent = 'Submitting...';
654
+
655
+ try {
656
+ const res = await fetch(`${API_BASE}/directory`, {
657
+ method: 'POST',
658
+ headers: { 'Content-Type': 'application/json' },
659
+ body: JSON.stringify({
660
+ domain: document.getElementById('submit-domain').value,
661
+ displayName: document.getElementById('submit-name').value || undefined,
662
+ description: document.getElementById('submit-description').value || undefined,
663
+ category: document.getElementById('submit-category').value || undefined,
664
+ countryCode: document.getElementById('submit-country').value || undefined,
665
+ }),
666
+ });
667
+
668
+ const data = await res.json();
669
+
670
+ if (!res.ok) {
671
+ throw new Error(data.details || data.error || 'Submission failed');
672
+ }
673
+
674
+ formMessage.className = 'form-message success';
675
+ formMessage.textContent = 'Store submitted successfully! It will appear in the directory shortly.';
676
+ formMessage.style.display = 'block';
677
+
678
+ // Reset form and reload
679
+ document.getElementById('submit-form').reset();
680
+ setTimeout(() => {
681
+ modal.classList.remove('active');
682
+ loadMerchants();
683
+ loadStats();
684
+ }, 2000);
685
+
686
+ } catch (error) {
687
+ formMessage.className = 'form-message error';
688
+ formMessage.textContent = error.message;
689
+ formMessage.style.display = 'block';
690
+ } finally {
691
+ submitBtn.disabled = false;
692
+ submitBtn.textContent = 'Submit Store';
693
+ }
694
+ });
695
+
696
+ // Initialize
697
+ loadStats();
698
+ loadMerchants();
699
+ </script>
700
+ </body>
701
+ </html>