tapimo 0.2.0 → 0.3.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 (313) hide show
  1. package/README.md +31 -3
  2. package/dist/ApiKey.d.ts +1 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +5 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +28 -1
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +34 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7167 -4507
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +8 -0
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +11 -2
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +14 -3
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +128 -118
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/apps/api-keys.d.ts +7 -0
  21. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.d.ts +8 -0
  23. package/dist/admin/apps/scopes.d.ts.map +1 -1
  24. package/dist/admin/apps/scopes.js +3 -0
  25. package/dist/admin/apps/scopes.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +13 -13
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/analytics/Analytics.d.ts +151 -0
  31. package/dist/analytics/Analytics.d.ts.map +1 -0
  32. package/dist/analytics/Analytics.js +212 -0
  33. package/dist/analytics/Analytics.js.map +1 -0
  34. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  36. package/dist/analytics/tables/requestEvents.js +264 -0
  37. package/dist/analytics/tables/requestEvents.js.map +1 -0
  38. package/dist/apps/data/App.d.ts +1024 -624
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +36 -0
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +45 -45
  43. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  44. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  45. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  46. package/dist/apps/data/routes/funding.d.ts +765 -0
  47. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  48. package/dist/apps/data/routes/funding.js +383 -0
  49. package/dist/apps/data/routes/funding.js.map +1 -0
  50. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  51. package/dist/apps/data/routes/receipts.js +2 -8
  52. package/dist/apps/data/routes/receipts.js.map +1 -1
  53. package/dist/apps/data/routes/rpc.d.ts +29 -36
  54. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  55. package/dist/apps/data/routes/rpc.js +29 -1
  56. package/dist/apps/data/routes/rpc.js.map +1 -1
  57. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  58. package/dist/apps/data/routes/transfers.d.ts +56 -56
  59. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  60. package/dist/apps/data/routes/webhooks.d.ts +45 -45
  61. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  62. package/dist/apps/data/routes/webhooks.js +8 -8
  63. package/dist/apps/data/routes/webhooks.js.map +1 -1
  64. package/dist/apps/docs/App.d.ts +4 -2
  65. package/dist/apps/docs/App.d.ts.map +1 -1
  66. package/dist/apps/docs/App.js +27 -95
  67. package/dist/apps/docs/App.js.map +1 -1
  68. package/dist/apps/docs/reference.d.ts +21 -0
  69. package/dist/apps/docs/reference.d.ts.map +1 -0
  70. package/dist/apps/docs/reference.js +112 -0
  71. package/dist/apps/docs/reference.js.map +1 -0
  72. package/dist/apps/index.d.ts +1 -0
  73. package/dist/apps/index.d.ts.map +1 -1
  74. package/dist/apps/index.js +1 -0
  75. package/dist/apps/index.js.map +1 -1
  76. package/dist/apps/mcp.d.ts +76 -0
  77. package/dist/apps/mcp.d.ts.map +1 -0
  78. package/dist/apps/mcp.js +188 -0
  79. package/dist/apps/mcp.js.map +1 -0
  80. package/dist/apps/platform/App.d.ts +3005 -728
  81. package/dist/apps/platform/App.d.ts.map +1 -1
  82. package/dist/apps/platform/App.js +13 -3
  83. package/dist/apps/platform/App.js.map +1 -1
  84. package/dist/apps/platform/Billing.d.ts +279 -0
  85. package/dist/apps/platform/Billing.d.ts.map +1 -0
  86. package/dist/apps/platform/Billing.js +477 -0
  87. package/dist/apps/platform/Billing.js.map +1 -0
  88. package/dist/apps/platform/index.d.ts +1 -0
  89. package/dist/apps/platform/index.d.ts.map +1 -1
  90. package/dist/apps/platform/index.js +1 -0
  91. package/dist/apps/platform/index.js.map +1 -1
  92. package/dist/apps/platform/routes/api-keys.d.ts +341 -49
  93. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
  94. package/dist/apps/platform/routes/api-keys.js +115 -13
  95. package/dist/apps/platform/routes/api-keys.js.map +1 -1
  96. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  97. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  98. package/dist/apps/platform/routes/billing.js +795 -0
  99. package/dist/apps/platform/routes/billing.js.map +1 -0
  100. package/dist/apps/platform/routes/invitations.d.ts +201 -83
  101. package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
  102. package/dist/apps/platform/routes/invitations.js +58 -0
  103. package/dist/apps/platform/routes/invitations.js.map +1 -1
  104. package/dist/apps/platform/routes/me.d.ts +17 -17
  105. package/dist/apps/platform/routes/orgs.d.ts +50 -36
  106. package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
  107. package/dist/apps/platform/routes/orgs.js +14 -3
  108. package/dist/apps/platform/routes/orgs.js.map +1 -1
  109. package/dist/apps/platform/routes/projects.d.ts +97 -97
  110. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  111. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  112. package/dist/apps/platform/routes/scopes.js +66 -0
  113. package/dist/apps/platform/routes/scopes.js.map +1 -0
  114. package/dist/apps/platform/routes/usage.d.ts +588 -0
  115. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  116. package/dist/apps/platform/routes/usage.js +391 -0
  117. package/dist/apps/platform/routes/usage.js.map +1 -0
  118. package/dist/apps/relay/App.d.ts +4 -1
  119. package/dist/apps/relay/App.d.ts.map +1 -1
  120. package/dist/apps/relay/App.js +118 -10
  121. package/dist/apps/relay/App.js.map +1 -1
  122. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  123. package/dist/apps/relay/Sponsorships.js +2 -1
  124. package/dist/apps/relay/Sponsorships.js.map +1 -1
  125. package/dist/cli/Cli.d.ts +8 -1
  126. package/dist/cli/Cli.d.ts.map +1 -1
  127. package/dist/cli/Cli.js +11 -3
  128. package/dist/cli/Cli.js.map +1 -1
  129. package/dist/cli/commands/admin.d.ts +4 -0
  130. package/dist/cli/commands/admin.d.ts.map +1 -1
  131. package/dist/cli/commands/admin.js +80 -1
  132. package/dist/cli/commands/admin.js.map +1 -1
  133. package/dist/db/Db.d.ts +7 -0
  134. package/dist/db/Db.d.ts.map +1 -1
  135. package/dist/db/Db.js +126 -0
  136. package/dist/db/Db.js.map +1 -1
  137. package/dist/db/Schema.d.ts +79 -0
  138. package/dist/db/Schema.d.ts.map +1 -1
  139. package/dist/db/Schema.js +88 -2
  140. package/dist/db/Schema.js.map +1 -1
  141. package/dist/db/tables/billingSettings.d.ts +55 -0
  142. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  143. package/dist/db/tables/billingSettings.js +70 -0
  144. package/dist/db/tables/billingSettings.js.map +1 -0
  145. package/dist/db/tables/invitations.js +2 -2
  146. package/dist/db/tables/invitations.js.map +1 -1
  147. package/dist/db/tables/organizations.d.ts +6 -2
  148. package/dist/db/tables/organizations.d.ts.map +1 -1
  149. package/dist/db/tables/organizations.js +10 -4
  150. package/dist/db/tables/organizations.js.map +1 -1
  151. package/dist/db/tables/projects.js +2 -2
  152. package/dist/db/tables/projects.js.map +1 -1
  153. package/dist/db/tables/requestUsage.d.ts +121 -0
  154. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  155. package/dist/db/tables/requestUsage.js +150 -0
  156. package/dist/db/tables/requestUsage.js.map +1 -0
  157. package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
  158. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  159. package/dist/db/tables/sponsoredTransactions.js +155 -2
  160. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  161. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  162. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  163. package/dist/db/tables/stripeCustomers.js +120 -0
  164. package/dist/db/tables/stripeCustomers.js.map +1 -0
  165. package/dist/db/tables/users.js +2 -2
  166. package/dist/db/tables/users.js.map +1 -1
  167. package/dist/handlers/internal/sponsorship.d.ts +5 -2
  168. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  169. package/dist/handlers/internal/sponsorship.js +19 -5
  170. package/dist/handlers/internal/sponsorship.js.map +1 -1
  171. package/dist/handlers/internal/utils.d.ts +10 -3
  172. package/dist/handlers/internal/utils.d.ts.map +1 -1
  173. package/dist/handlers/internal/utils.js +14 -6
  174. package/dist/handlers/internal/utils.js.map +1 -1
  175. package/dist/index.d.ts +1 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +1 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/internal/Auth.d.ts +18 -4
  180. package/dist/internal/Auth.d.ts.map +1 -1
  181. package/dist/internal/Auth.js +148 -76
  182. package/dist/internal/Auth.js.map +1 -1
  183. package/dist/internal/Cache.d.ts.map +1 -1
  184. package/dist/internal/Cache.js +4 -1
  185. package/dist/internal/Cache.js.map +1 -1
  186. package/dist/internal/EdgeCache.d.ts +10 -0
  187. package/dist/internal/EdgeCache.d.ts.map +1 -1
  188. package/dist/internal/EdgeCache.js +39 -8
  189. package/dist/internal/EdgeCache.js.map +1 -1
  190. package/dist/internal/Fees.d.ts +27 -0
  191. package/dist/internal/Fees.d.ts.map +1 -0
  192. package/dist/internal/Fees.js +48 -0
  193. package/dist/internal/Fees.js.map +1 -0
  194. package/dist/internal/FundingRoutes.d.ts +183 -0
  195. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  196. package/dist/internal/FundingRoutes.js +490 -0
  197. package/dist/internal/FundingRoutes.js.map +1 -0
  198. package/dist/internal/Id.d.ts +15 -0
  199. package/dist/internal/Id.d.ts.map +1 -0
  200. package/dist/internal/Id.js +22 -0
  201. package/dist/internal/Id.js.map +1 -0
  202. package/dist/internal/Log.d.ts +2 -0
  203. package/dist/internal/Log.d.ts.map +1 -1
  204. package/dist/internal/Log.js +3 -0
  205. package/dist/internal/Log.js.map +1 -1
  206. package/dist/internal/OpenApi.d.ts +10 -1
  207. package/dist/internal/OpenApi.d.ts.map +1 -1
  208. package/dist/internal/OpenApi.js +8 -1
  209. package/dist/internal/OpenApi.js.map +1 -1
  210. package/dist/internal/RateLimit.d.ts +25 -0
  211. package/dist/internal/RateLimit.d.ts.map +1 -1
  212. package/dist/internal/RateLimit.js +51 -0
  213. package/dist/internal/RateLimit.js.map +1 -1
  214. package/dist/internal/Response.d.ts +1 -1
  215. package/dist/internal/Response.d.ts.map +1 -1
  216. package/dist/internal/Schema.d.ts +8 -0
  217. package/dist/internal/Schema.d.ts.map +1 -1
  218. package/dist/internal/Schema.js +7 -0
  219. package/dist/internal/Schema.js.map +1 -1
  220. package/dist/internal/Webhooks.d.ts.map +1 -1
  221. package/dist/internal/Webhooks.js +3 -3
  222. package/dist/internal/Webhooks.js.map +1 -1
  223. package/package.json +4 -3
  224. package/src/ApiKey.ts +9 -2
  225. package/src/ApiKeys.test.ts +51 -0
  226. package/src/ApiKeys.ts +50 -1
  227. package/src/App.test.ts +33 -86718
  228. package/src/App.ts +18 -0
  229. package/src/Client.test-d.ts +351 -0
  230. package/src/Scope.ts +16 -3
  231. package/src/admin/apps/scopes.test.ts +6 -0
  232. package/src/admin/apps/scopes.ts +3 -0
  233. package/src/admin/ui/src/components/Page.tsx +2 -3
  234. package/src/admin/ui/src/routes/__root.tsx +2 -2
  235. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  236. package/src/admin/ui.generated.ts +13 -13
  237. package/src/analytics/Analytics.test.ts +527 -0
  238. package/src/analytics/Analytics.ts +318 -0
  239. package/src/analytics/tables/requestEvents.ts +523 -0
  240. package/src/apps/data/App.test.ts +72 -0
  241. package/src/apps/data/App.ts +35 -0
  242. package/src/apps/data/routes/funding.test.ts +214 -0
  243. package/src/apps/data/routes/funding.ts +584 -0
  244. package/src/apps/data/routes/receipts.ts +2 -9
  245. package/src/apps/data/routes/rpc.test.ts +78 -2
  246. package/src/apps/data/routes/rpc.ts +31 -1
  247. package/src/apps/data/routes/webhooks.test.ts +1 -1
  248. package/src/apps/data/routes/webhooks.ts +11 -8
  249. package/src/apps/docs/App.ts +29 -100
  250. package/src/apps/docs/reference.ts +132 -0
  251. package/src/apps/index.ts +1 -0
  252. package/src/apps/mcp.test.ts +206 -0
  253. package/src/apps/mcp.ts +250 -0
  254. package/src/apps/platform/App.ts +43 -4
  255. package/src/apps/platform/Billing.test.ts +530 -0
  256. package/src/apps/platform/Billing.ts +682 -0
  257. package/src/apps/platform/index.ts +1 -0
  258. package/src/apps/platform/routes/api-keys.test.ts +172 -9
  259. package/src/apps/platform/routes/api-keys.ts +136 -20
  260. package/src/apps/platform/routes/billing.test.ts +1100 -0
  261. package/src/apps/platform/routes/billing.ts +966 -0
  262. package/src/apps/platform/routes/invitations.test.ts +55 -7
  263. package/src/apps/platform/routes/invitations.ts +68 -0
  264. package/src/apps/platform/routes/me.test.ts +230 -0
  265. package/src/apps/platform/routes/orgs.ts +26 -4
  266. package/src/apps/platform/routes/scopes.test.ts +29 -0
  267. package/src/apps/platform/routes/scopes.ts +85 -0
  268. package/src/apps/platform/routes/usage.test.ts +422 -0
  269. package/src/apps/platform/routes/usage.ts +527 -0
  270. package/src/apps/relay/App.test.ts +457 -8
  271. package/src/apps/relay/App.ts +120 -12
  272. package/src/apps/relay/Sponsorships.test.ts +1 -1
  273. package/src/apps/relay/Sponsorships.ts +2 -1
  274. package/src/cli/Cli.test.ts +25 -2
  275. package/src/cli/Cli.ts +17 -2
  276. package/src/cli/commands/admin.test.ts +83 -0
  277. package/src/cli/commands/admin.ts +89 -1
  278. package/src/db/Db.ts +137 -0
  279. package/src/db/Schema.ts +104 -2
  280. package/src/db/tables/billingSettings.test.ts +87 -0
  281. package/src/db/tables/billingSettings.ts +108 -0
  282. package/src/db/tables/invitations.ts +2 -2
  283. package/src/db/tables/organizations.test.ts +1 -1
  284. package/src/db/tables/organizations.ts +15 -5
  285. package/src/db/tables/projects.ts +2 -2
  286. package/src/db/tables/requestUsage.ts +231 -0
  287. package/src/db/tables/sponsoredTransactions.test.ts +108 -1
  288. package/src/db/tables/sponsoredTransactions.ts +230 -2
  289. package/src/db/tables/stripeCustomers.test.ts +109 -0
  290. package/src/db/tables/stripeCustomers.ts +162 -0
  291. package/src/db/tables/users.ts +2 -2
  292. package/src/handlers/internal/sponsorship.ts +34 -7
  293. package/src/handlers/internal/utils.ts +19 -7
  294. package/src/index.ts +1 -0
  295. package/src/internal/Auth.test.ts +175 -25
  296. package/src/internal/Auth.ts +175 -86
  297. package/src/internal/Cache.ts +5 -1
  298. package/src/internal/EdgeCache.test.ts +27 -4
  299. package/src/internal/EdgeCache.ts +47 -9
  300. package/src/internal/Fees.test.ts +32 -0
  301. package/src/internal/Fees.ts +51 -0
  302. package/src/internal/FundingRoutes.ts +583 -0
  303. package/src/internal/Id.test.ts +17 -0
  304. package/src/internal/Id.ts +24 -0
  305. package/src/internal/Log.test.ts +1 -0
  306. package/src/internal/Log.ts +5 -0
  307. package/src/internal/OpenApi.ts +12 -1
  308. package/src/internal/RateLimit.test.ts +84 -0
  309. package/src/internal/RateLimit.ts +70 -0
  310. package/src/internal/Response.ts +1 -1
  311. package/src/internal/Schema.ts +8 -0
  312. package/src/internal/Webhooks.test.ts +3 -3
  313. package/src/internal/Webhooks.ts +3 -3
@@ -0,0 +1,523 @@
1
+ import type * as Log from '../../internal/Log.js'
2
+ import type * as Metrics from '../../Metrics.js'
3
+ import type * as Analytics from '../Analytics.js'
4
+
5
+ /** Columns of the `request_events` table: one compact row per API request. */
6
+ export type Table = {
7
+ /**
8
+ * Whether the owning org's billing was active at request time (`1`/`0`), for
9
+ * authenticated API-key callers; `null` otherwise. The request-metered
10
+ * billing gate for sandbox: only `1` rows bill.
11
+ */
12
+ billing_active: number | null
13
+ /** Total request duration in milliseconds. */
14
+ duration_ms: number
15
+ /** Deployment environment that served the request. */
16
+ environment: string
17
+ /** Stable API error code, when the response was an error envelope. */
18
+ error_code: string | null
19
+ /** API-key environment for authenticated API-key callers. */
20
+ key_environment: 'production' | 'sandbox' | null
21
+ /** API key id for authenticated API-key callers. */
22
+ key_id: string | null
23
+ /** HTTP method. */
24
+ method: string
25
+ /** Organization id for authenticated API-key callers. */
26
+ org_id: string | null
27
+ /** Project id for authenticated API-key callers. */
28
+ project_id: string | null
29
+ /** Caller principal kind. */
30
+ principal_type: 'api_key' | 'public' | 'session' | 'super_admin' | 'unknown'
31
+ /** Rate-limit bucket scope, when a rate limit was applied. */
32
+ rate_limit_scope: string | null
33
+ /** Stable per-request id (`entry.requestId`); dedups the billing count via `uniqExact`. */
34
+ request_id: string | null
35
+ /** Bounded matched route pattern. */
36
+ route: string
37
+ /** Service that emitted the event. */
38
+ service: string
39
+ /** HTTP response status code. */
40
+ status: number
41
+ /** UTC request completion timestamp formatted for ClickHouse DateTime64. */
42
+ timestamp: string
43
+ }
44
+
45
+ /** One request attribution term for usage reads. */
46
+ export type Attribution = {
47
+ /** API key ids matched by this term; absent means all keys. */
48
+ apiKeyIds?: readonly string[] | undefined
49
+ /** API-key environment matched by this term; absent means all environments. */
50
+ environment?: Environment | undefined
51
+ /**
52
+ * Project scope for this term: a project id matches that project, `null`
53
+ * matches rows without project attribution, and `undefined` matches the
54
+ * whole organization (any project, including unattributed rows).
55
+ */
56
+ projectId?: string | null | undefined
57
+ }
58
+
59
+ /** API-key environment for request usage. */
60
+ export type Environment = 'production' | 'sandbox'
61
+
62
+ /** API key usage grouped for one project. */
63
+ export type KeyBreakdown = {
64
+ /** API key id (`key_…`). */
65
+ apiKeyId: string
66
+ /** Average request duration in milliseconds. */
67
+ averageDurationMs: number
68
+ /** Requests whose status was at least 400. */
69
+ errors: number
70
+ /** Total requests. */
71
+ requests: number
72
+ }
73
+
74
+ /** Time bucket size for the usage series. */
75
+ export type Interval = 'day' | 'hour'
76
+
77
+ /** Route usage grouped for one project. */
78
+ export type RouteBreakdown = {
79
+ /** Average request duration in milliseconds. */
80
+ averageDurationMs: number
81
+ /** Requests whose status was at least 400. */
82
+ errors: number
83
+ /** Total requests. */
84
+ requests: number
85
+ /** Matched route pattern. */
86
+ route: string
87
+ }
88
+
89
+ /** One usage series point. */
90
+ export type SeriesPoint = {
91
+ /** Requests whose status was at least 400. */
92
+ errors: number
93
+ /** Total requests. */
94
+ requests: number
95
+ /** Bucket start timestamp (ISO 8601). */
96
+ time: string
97
+ }
98
+
99
+ /** Status-code usage grouped for one project. */
100
+ export type StatusBreakdown = {
101
+ /** Total requests. */
102
+ requests: number
103
+ /** HTTP status code. */
104
+ status: number
105
+ }
106
+
107
+ /** Overall usage totals for one project. */
108
+ export type Totals = {
109
+ /** Average request duration in milliseconds. */
110
+ averageDurationMs: number
111
+ /** Requests whose status was at least 400. */
112
+ errors: number
113
+ /** Total requests. */
114
+ requests: number
115
+ }
116
+
117
+ /** Usage read options for one project. */
118
+ export type ProjectUsageOptions = {
119
+ /** Attribution terms that define which request rows count. */
120
+ attributions: readonly Attribution[]
121
+ /** Inclusive lower timestamp bound (ISO 8601). */
122
+ from: string
123
+ /** Bucket size for the time series. */
124
+ interval: Interval
125
+ /** Organization id (`org_…`). */
126
+ orgId: string
127
+ /** Inclusive upper timestamp bound (ISO 8601). */
128
+ to: string
129
+ }
130
+
131
+ /** Project usage response body. */
132
+ export type ProjectUsage = {
133
+ /** Usage grouped by API key. */
134
+ byKey: readonly KeyBreakdown[]
135
+ /** Usage grouped by route. */
136
+ byRoute: readonly RouteBreakdown[]
137
+ /** Usage grouped by status code. */
138
+ byStatus: readonly StatusBreakdown[]
139
+ /** Inclusive lower timestamp bound used for the read (ISO 8601). */
140
+ from: string
141
+ /** Bucket size used for the time series. */
142
+ interval: Interval
143
+ /** Usage grouped into small time buckets. */
144
+ series: readonly SeriesPoint[]
145
+ /** Inclusive upper timestamp bound used for the read (ISO 8601). */
146
+ to: string
147
+ /** Overall usage totals. */
148
+ totals: Totals
149
+ }
150
+
151
+ /**
152
+ * Builds a `Metrics.cloudflare` analytics factory that enqueues one compact
153
+ * request row per entry onto `queue`. Pass the result directly as the
154
+ * backend's `analytics` option; it reuses the backend's
155
+ * `enabled`/`environment`/`service` context and self-disables when `enabled`
156
+ * is false.
157
+ *
158
+ * @param queue - The analytics queue producer.
159
+ * @returns The analytics factory.
160
+ */
161
+ export function createQueueSink(queue: createQueueSink.Queue): createQueueSink.Factory {
162
+ return (context) => async (entry) => {
163
+ if (!context.enabled) return
164
+ const row = event(entry, context)
165
+ try {
166
+ await queue.send(row)
167
+ } catch {
168
+ // Transient Queues internal errors (e.g. code 15000) usually clear on retry.
169
+ await queue.send(row)
170
+ }
171
+ }
172
+ }
173
+
174
+ export declare namespace createQueueSink {
175
+ /** Analytics factory accepted by `Metrics.cloudflare`'s `analytics` option. */
176
+ type Factory = (context: Metrics.cloudflare.Context) => Log.Emit
177
+
178
+ /** Minimal Cloudflare Queue producer shape the sink sends rows to. */
179
+ type Queue = {
180
+ /** Enqueues one row; the result is awaited and discarded. */
181
+ send(message: Table): Promise<unknown>
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Inserts a batch of queued request rows, acking each message on success and
187
+ * retrying the batch on failure.
188
+ *
189
+ * @param analytics - The analytics store.
190
+ * @param messages - The queued messages.
191
+ */
192
+ export async function insertMessages(
193
+ analytics: Analytics.Analytics,
194
+ messages: readonly insertMessages.Message[],
195
+ ): Promise<void> {
196
+ if (messages.length === 0) return
197
+
198
+ try {
199
+ await analytics.insert(
200
+ 'request_events',
201
+ messages.map((message) => message.body),
202
+ )
203
+ for (const message of messages) message.ack()
204
+ } catch (error) {
205
+ console.error('ClickHouse analytics insert failed', error)
206
+ for (const message of messages) message.retry()
207
+ }
208
+ }
209
+
210
+ export declare namespace insertMessages {
211
+ /** Minimal Cloudflare Queue message shape carrying one row. */
212
+ type Message = {
213
+ /** Acknowledges the message. */
214
+ ack(): void
215
+ /** Queued row. */
216
+ body: Table
217
+ /** Marks the message for redelivery. */
218
+ retry(): void
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Reads request usage for one project from the `request_events` table.
224
+ *
225
+ * @param analytics - The analytics store.
226
+ * @param options - The usage read options.
227
+ * @returns The project usage.
228
+ */
229
+ export async function readProjectUsage(
230
+ analytics: Analytics.Analytics,
231
+ options: ProjectUsageOptions,
232
+ ): Promise<ProjectUsage> {
233
+ const where = usageWhere(options)
234
+ const average = `toFloat64(coalesce(round(avgOrNull(duration_ms), 1), 0))`
235
+ const bucket = options.interval === 'day' ? 'toStartOfDay(timestamp)' : 'toStartOfHour(timestamp)'
236
+ const [byKey, byRoute, byStatus, series, totals] = await Promise.all([
237
+ analytics.query<KeyRow>(
238
+ `
239
+ SELECT
240
+ key_id,
241
+ count() AS requests,
242
+ countIf(status >= 400) AS errors,
243
+ ${average} AS averageDurationMs
244
+ FROM ${table}
245
+ WHERE ${where} AND key_id IS NOT NULL
246
+ GROUP BY key_id
247
+ ORDER BY requests DESC, key_id ASC
248
+ LIMIT 100
249
+ `,
250
+ ),
251
+ analytics.query<RouteRow>(
252
+ `
253
+ SELECT
254
+ route,
255
+ count() AS requests,
256
+ countIf(status >= 400) AS errors,
257
+ ${average} AS averageDurationMs
258
+ FROM ${table}
259
+ WHERE ${where}
260
+ GROUP BY route
261
+ ORDER BY requests DESC, route ASC
262
+ LIMIT 100
263
+ `,
264
+ ),
265
+ analytics.query<StatusRow>(
266
+ `
267
+ SELECT
268
+ status,
269
+ count() AS requests
270
+ FROM ${table}
271
+ WHERE ${where}
272
+ GROUP BY status
273
+ ORDER BY status ASC
274
+ `,
275
+ ),
276
+ analytics.query<SeriesRow>(
277
+ `
278
+ SELECT
279
+ formatDateTime(${bucket}, '%Y-%m-%dT%H:%i:%S.000Z', 'UTC') AS time,
280
+ count() AS requests,
281
+ countIf(status >= 400) AS errors
282
+ FROM ${table}
283
+ WHERE ${where}
284
+ GROUP BY ${bucket}
285
+ ORDER BY ${bucket} ASC
286
+ `,
287
+ ),
288
+ analytics.query<TotalsRow>(
289
+ `
290
+ SELECT
291
+ count() AS requests,
292
+ countIf(status >= 400) AS errors,
293
+ ${average} AS averageDurationMs
294
+ FROM ${table}
295
+ WHERE ${where}
296
+ `,
297
+ ),
298
+ ])
299
+ return {
300
+ byKey: byKey.map((row) => ({
301
+ apiKeyId: String(row.key_id),
302
+ averageDurationMs: number(row.averageDurationMs),
303
+ errors: number(row.errors),
304
+ requests: number(row.requests),
305
+ })),
306
+ byRoute: byRoute.map((row) => ({
307
+ averageDurationMs: number(row.averageDurationMs),
308
+ errors: number(row.errors),
309
+ requests: number(row.requests),
310
+ route: String(row.route),
311
+ })),
312
+ byStatus: byStatus.map((row) => ({
313
+ requests: number(row.requests),
314
+ status: number(row.status),
315
+ })),
316
+ from: options.from,
317
+ interval: options.interval,
318
+ series: series.map((row) => ({
319
+ errors: number(row.errors),
320
+ requests: number(row.requests),
321
+ time: String(row.time),
322
+ })),
323
+ to: options.to,
324
+ totals: totals[0]
325
+ ? {
326
+ averageDurationMs: number(totals[0].averageDurationMs),
327
+ errors: number(totals[0].errors),
328
+ requests: number(totals[0].requests),
329
+ }
330
+ : { averageDurationMs: 0, errors: 0, requests: 0 },
331
+ }
332
+ }
333
+
334
+ /** One org's deduped billable request count for a single UTC hour. */
335
+ export type BillableCount = {
336
+ /** UTC hour start (ISO 8601). */
337
+ bucketStart: string
338
+ /** Deduped billable request count (`uniqExact(request_id)`). */
339
+ count: number
340
+ /** Organization id (`org_…`). */
341
+ orgId: string
342
+ }
343
+
344
+ /** Options for {@link readBillableCounts}. */
345
+ export type BillableCountsOptions = {
346
+ /** API-key environment to bill; `sandbox` rows require request-time billing. */
347
+ environment: Environment
348
+ /** Inclusive lower timestamp bound (ISO 8601). */
349
+ from: string
350
+ /** Exclusive upper timestamp bound (ISO 8601); the seal cutoff. */
351
+ to: string
352
+ }
353
+
354
+ /**
355
+ * Reads deduped billable request counts grouped by `(org, UTC hour)` for the
356
+ * request-metered billing reporter. Counts dedupe on `request_id` via
357
+ * `uniqExact` (the queue → ClickHouse path can duplicate rows); production rows
358
+ * always bill, sandbox rows only when `billing_active = 1` at request time.
359
+ *
360
+ * @param analytics - The analytics store.
361
+ * @param options - The read options.
362
+ * @returns One deduped count per `(org, hour)`.
363
+ */
364
+ export async function readBillableCounts(
365
+ analytics: Analytics.Analytics,
366
+ options: BillableCountsOptions,
367
+ ): Promise<BillableCount[]> {
368
+ const sandboxGate = options.environment === 'production' ? '' : `AND billing_active = 1`
369
+ const rows = await analytics.query<BillableCountRow>(
370
+ `
371
+ SELECT
372
+ org_id AS orgId,
373
+ formatDateTime(toStartOfHour(timestamp), '%Y-%m-%dT%H:%i:%S.000Z', 'UTC') AS bucketStart,
374
+ uniqExact(request_id) AS count
375
+ FROM ${table}
376
+ WHERE principal_type = 'api_key'
377
+ AND org_id IS NOT NULL
378
+ AND request_id IS NOT NULL
379
+ AND key_environment = ${literal(options.environment)}
380
+ ${sandboxGate}
381
+ AND timestamp >= parseDateTime64BestEffort(${literal(options.from)}, 3, 'UTC')
382
+ AND timestamp < parseDateTime64BestEffort(${literal(options.to)}, 3, 'UTC')
383
+ GROUP BY org_id, toStartOfHour(timestamp)
384
+ `,
385
+ )
386
+ return rows.map((row) => ({
387
+ bucketStart: String(row.bucketStart),
388
+ count: number(row.count),
389
+ orgId: String(row.orgId),
390
+ }))
391
+ }
392
+
393
+ type BillableCountRow = {
394
+ bucketStart: string
395
+ count: number | string
396
+ orgId: string
397
+ }
398
+
399
+ /** Table name queries read from; the store scopes the database. */
400
+ const table = 'request_events'
401
+
402
+ type KeyRow = {
403
+ averageDurationMs: number | string
404
+ errors: number | string
405
+ key_id: string
406
+ requests: number | string
407
+ }
408
+
409
+ type RouteRow = {
410
+ averageDurationMs: number | string
411
+ errors: number | string
412
+ requests: number | string
413
+ route: string
414
+ }
415
+
416
+ type SeriesRow = {
417
+ errors: number | string
418
+ requests: number | string
419
+ time: string
420
+ }
421
+
422
+ type StatusRow = {
423
+ requests: number | string
424
+ status: number | string
425
+ }
426
+
427
+ type TotalsRow = {
428
+ averageDurationMs: number | string
429
+ errors: number | string
430
+ requests: number | string
431
+ }
432
+
433
+ /** Maps one request log entry to a stored row. */
434
+ function event(entry: Log.Entry, context: Metrics.cloudflare.Context): Table {
435
+ const principal = entry.principal
436
+ return {
437
+ billing_active: principal?.type === 'api_key' ? Number(principal.billingActive === true) : null,
438
+ duration_ms: entry.duration,
439
+ environment: context.environment ?? 'unknown',
440
+ error_code: entry.errorCode ?? null,
441
+ key_environment: principal?.type === 'api_key' ? (principal.environment ?? null) : null,
442
+ key_id: principal?.type === 'api_key' ? principal.id : null,
443
+ method: entry.method,
444
+ org_id: principal?.type === 'api_key' ? (principal.orgId ?? null) : null,
445
+ project_id: principal?.type === 'api_key' ? (principal.projectId ?? null) : null,
446
+ principal_type: principal?.type ?? 'unknown',
447
+ rate_limit_scope: entry.rateLimit?.scope ?? null,
448
+ request_id: entry.requestId ?? null,
449
+ route: entry.route,
450
+ service: context.service ?? 'unknown',
451
+ status: entry.status,
452
+ timestamp: timestamp(),
453
+ }
454
+ }
455
+
456
+ function usageWhere(options: ProjectUsageOptions) {
457
+ const attributions = options.attributions.length
458
+ ? options.attributions
459
+ .map((attribution) => `(${attributionWhere(attribution, options)})`)
460
+ .join(' OR ')
461
+ : '0'
462
+ return [
463
+ `principal_type = 'api_key'`,
464
+ `(${attributions})`,
465
+ `timestamp >= parseDateTime64BestEffort(${literal(options.from)}, 3, 'UTC')`,
466
+ `timestamp <= parseDateTime64BestEffort(${literal(options.to)}, 3, 'UTC')`,
467
+ ].join(' AND ')
468
+ }
469
+
470
+ function attributionWhere(attribution: Attribution, options: ProjectUsageOptions) {
471
+ if (attribution.projectId === null) {
472
+ if (!attribution.apiKeyIds?.length) return '0'
473
+ return [
474
+ `project_id IS NULL`,
475
+ `key_id IN (${attribution.apiKeyIds.map(literal).join(', ')})`,
476
+ ...(attribution.environment === undefined
477
+ ? []
478
+ : [keyEnvironmentWhere(attribution.environment, true)]),
479
+ ].join(' AND ')
480
+ }
481
+
482
+ // Absent `projectId` scopes to the whole organization (any project).
483
+ if (attribution.projectId === undefined) {
484
+ return [
485
+ `org_id = ${literal(options.orgId)}`,
486
+ ...(attribution.apiKeyIds === undefined
487
+ ? []
488
+ : [`key_id IN (${attribution.apiKeyIds.map(literal).join(', ')})`]),
489
+ ...(attribution.environment === undefined
490
+ ? []
491
+ : [keyEnvironmentWhere(attribution.environment, false)]),
492
+ ].join(' AND ')
493
+ }
494
+
495
+ return [
496
+ `org_id = ${literal(options.orgId)}`,
497
+ `project_id = ${literal(attribution.projectId)}`,
498
+ ...(attribution.apiKeyIds === undefined
499
+ ? []
500
+ : [`key_id IN (${attribution.apiKeyIds.map(literal).join(', ')})`]),
501
+ ...(attribution.environment === undefined
502
+ ? []
503
+ : [keyEnvironmentWhere(attribution.environment, false)]),
504
+ ].join(' AND ')
505
+ }
506
+
507
+ function keyEnvironmentWhere(environment: Environment, allowLegacyNull: boolean) {
508
+ const exact = `key_environment = ${literal(environment)}`
509
+ if (!allowLegacyNull) return exact
510
+ return `(${exact} OR key_environment IS NULL)`
511
+ }
512
+
513
+ function literal(value: string) {
514
+ return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
515
+ }
516
+
517
+ function number(value: number | string) {
518
+ return Number(value)
519
+ }
520
+
521
+ function timestamp() {
522
+ return new Date().toISOString().replace('T', ' ').replace('Z', '')
523
+ }
@@ -0,0 +1,72 @@
1
+ import * as TestApp from '../../../test/App.js'
2
+ import * as Viem from '../../internal/Viem.js'
3
+
4
+ /** A supported non-mainnet chain id, exercising the sandbox testnet allowance. */
5
+ const testnet = Viem.chainId.testnet
6
+
7
+ /** Sandbox reader: testnet-only across the chain-data routes. */
8
+ const sandbox = {
9
+ environment: 'sandbox',
10
+ id: 'key_sandbox',
11
+ orgId: 'org_test',
12
+ scopes: ['data:read'],
13
+ token: 'secret_sandbox',
14
+ } satisfies TestApp.kvStore.Key
15
+
16
+ /** Production reader: unaffected by the sandbox chain guard. */
17
+ const production = {
18
+ environment: 'production',
19
+ id: 'key_prod',
20
+ orgId: 'org_test',
21
+ scopes: ['data:read'],
22
+ token: 'secret_prod',
23
+ } satisfies TestApp.kvStore.Key
24
+
25
+ /** RequestInit carrying a bearer token for the given key. */
26
+ function as(key: { token: string }) {
27
+ return { headers: { authorization: `Bearer ${key.token}` } } as const
28
+ }
29
+
30
+ /** A deployment whose data group defaults to mainnet when `chainId` is absent. */
31
+ function mainnetDefaultClient() {
32
+ return TestApp.client({
33
+ auth: { keys: [sandbox, production] },
34
+ defaultChainId: Viem.chainId.mainnet,
35
+ })
36
+ }
37
+
38
+ describe('sandbox chain guard', () => {
39
+ test('rejects a sandbox key that omits `chainId` on a mainnet-default deployment', async () => {
40
+ const response = await mainnetDefaultClient().v1.tokenlist.$get({ query: {} }, as(sandbox))
41
+ expect(response.status).toBe(403)
42
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
43
+ 'api_key_forbidden',
44
+ )
45
+ })
46
+
47
+ test('rejects a sandbox key on an explicit mainnet `chainId`', async () => {
48
+ const client = TestApp.client({ auth: { keys: [sandbox, production] } })
49
+ const response = await client.v1.tokenlist.$get(
50
+ { query: { chainId: String(Viem.chainId.mainnet) } },
51
+ as(sandbox),
52
+ )
53
+ expect(response.status).toBe(403)
54
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
55
+ 'api_key_forbidden',
56
+ )
57
+ })
58
+
59
+ test('allows a sandbox key on a testnet `chainId`', async () => {
60
+ const client = mainnetDefaultClient()
61
+ const response = await client.v1.tokenlist.$get(
62
+ { query: { chainId: String(testnet) } },
63
+ as(sandbox),
64
+ )
65
+ expect(response.status).toBe(200)
66
+ })
67
+
68
+ test('leaves production keys unaffected on a mainnet-default deployment', async () => {
69
+ const response = await mainnetDefaultClient().v1.tokenlist.$get({ query: {} }, as(production))
70
+ expect(response.status).toBe(200)
71
+ })
72
+ })
@@ -17,6 +17,7 @@ import { blocks, blocksByTimestampCompat } from './routes/blocks.js'
17
17
  import { coingecko } from './routes/coingecko.js'
18
18
  import { exchanges } from './routes/exchanges.js'
19
19
  import { feeAmm } from './routes/fee-amm.js'
20
+ import { funding } from './routes/funding.js'
20
21
  import { indexer } from './routes/indexer.js'
21
22
  import { receipts } from './routes/receipts.js'
22
23
  import { rpc } from './routes/rpc.js'
@@ -94,6 +95,34 @@ export function data(options: data.Options = {}) {
94
95
  if (!parsed.success || supportedChainIds.has(parsed.data)) return next()
95
96
  return Response.unsupportedChainId(c, parsed.data, supportedChainIds)
96
97
  })
98
+ // Sandbox keys are testnet-only. A chain-scoped data route defaults to
99
+ // mainnet, so a sandbox key that omits `chainId` (or targets mainnet) is
100
+ // refused with a clear message telling the caller to pass a testnet
101
+ // `chainId`. The relay resolves its chain from the request body, not the
102
+ // query, so it is skipped and enforces the rule in its sponsor validation.
103
+ .use('*', async (c, next) => {
104
+ const relay = Path.join(c.get('basePath') ?? '', 'rpc/relay')
105
+ if (c.req.path === relay || c.req.path.startsWith(`${relay}/`)) return next()
106
+
107
+ const principal = Auth.getPrincipal(c)
108
+ if (principal?.type !== 'api_key' || principal.environment !== 'sandbox') return next()
109
+
110
+ const raw = c.req.query('chainId') ?? c.req.query('chain_id')
111
+ // Absent falls back to the group default (mainnet); a malformed value
112
+ // stays with the route's own validator rather than the sandbox message.
113
+ const chainId = (() => {
114
+ if (raw === undefined) return defaultChainId
115
+ const parsed = Schema.ChainId.safeParse(raw)
116
+ return parsed.success ? parsed.data : undefined
117
+ })()
118
+ if (chainId === undefined || !Viem.isMainnet(chainId)) return next()
119
+
120
+ return Response.error(c, {
121
+ code: 'api_key_forbidden',
122
+ message: 'Sandbox API keys only support testnet. Pass a testnet `chainId`.',
123
+ status: 403,
124
+ })
125
+ })
97
126
  // Serve static API assets (e.g. verified-token icons) from the configured
98
127
  // store; a no-op fall-through when no store is configured (logo URIs simply
99
128
  // go unresolved).
@@ -111,6 +140,7 @@ export function data(options: data.Options = {}) {
111
140
  .route('/', blocks())
112
141
  .route('/', exchanges())
113
142
  .route('/', feeAmm())
143
+ .route('/', funding())
114
144
  .route('/', indexer())
115
145
  .route('/', receipts())
116
146
  .route('/', tokenlist())
@@ -178,6 +208,8 @@ const openapi = {
178
208
  { name: 'Blocks', description: 'The ordered batches of transactions making up the chain.' },
179
209
  { name: 'Exchange', description: "Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices." }, // prettier-ignore
180
210
  { name: 'Fee AMM', description: 'Pools that convert stablecoins to pay fees.' },
211
+ { name: 'Funding', description: 'Routes and inventory for moving stablecoins into Tempo.' },
212
+ { name: 'Providers', description: 'Provider metadata, capabilities, and integration status.' },
181
213
  { name: 'Tokens', description: 'TIP-20 token details, supply, and holders.' },
182
214
  { name: 'Transactions', description: 'Transactions submitted to Tempo, and their receipts.' },
183
215
  { name: 'Transfers', description: 'Token movements from executing transactions.' },
@@ -196,6 +228,8 @@ const openapi = {
196
228
  'Blocks',
197
229
  'Exchange',
198
230
  'Fee AMM',
231
+ 'Funding',
232
+ 'Providers',
199
233
  'Tokens',
200
234
  'Transactions',
201
235
  'Transfers',
@@ -203,6 +237,7 @@ const openapi = {
203
237
  'Webhooks',
204
238
  'Indexer',
205
239
  'RPC',
240
+ 'MCP',
206
241
  'CoinGecko',
207
242
  ],
208
243
  },