tapimo 0.5.2 → 0.6.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 (299) hide show
  1. package/dist/App.d.ts +13987 -5755
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/admin/App.d.ts +910 -412
  5. package/dist/admin/App.d.ts.map +1 -1
  6. package/dist/admin/App.js +4 -0
  7. package/dist/admin/App.js.map +1 -1
  8. package/dist/admin/apps/early-access.d.ts +148 -0
  9. package/dist/admin/apps/early-access.d.ts.map +1 -0
  10. package/dist/admin/apps/early-access.js +133 -0
  11. package/dist/admin/apps/early-access.js.map +1 -0
  12. package/dist/admin/apps/verified-tokens.d.ts +643 -118
  13. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  14. package/dist/admin/apps/verified-tokens.js +214 -1
  15. package/dist/admin/apps/verified-tokens.js.map +1 -1
  16. package/dist/admin/ui.generated.d.ts +1 -1
  17. package/dist/admin/ui.generated.d.ts.map +1 -1
  18. package/dist/admin/ui.generated.js +45 -33
  19. package/dist/admin/ui.generated.js.map +1 -1
  20. package/dist/apps/data/App.d.ts +3733 -2428
  21. package/dist/apps/data/App.d.ts.map +1 -1
  22. package/dist/apps/data/App.js +11 -6
  23. package/dist/apps/data/App.js.map +1 -1
  24. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  25. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  26. package/dist/apps/data/FundingQuoteProviders.js +193 -2
  27. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  28. package/dist/apps/data/FxOracle.d.ts +71 -0
  29. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  30. package/dist/apps/data/FxOracle.js +97 -0
  31. package/dist/apps/data/FxOracle.js.map +1 -0
  32. package/dist/apps/data/index.d.ts +1 -0
  33. package/dist/apps/data/index.d.ts.map +1 -1
  34. package/dist/apps/data/index.js +1 -0
  35. package/dist/apps/data/index.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +3575 -1358
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +230 -88
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +963 -342
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/addresses.js +11 -9
  43. package/dist/apps/data/routes/addresses.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +108 -57
  45. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.js +88 -45
  47. package/dist/apps/data/routes/balances.js.map +1 -1
  48. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  49. package/dist/apps/data/routes/exchanges.d.ts +952 -201
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +518 -76
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  54. package/dist/apps/data/routes/funding.d.ts +8 -2
  55. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  56. package/dist/apps/data/routes/funding.js +32 -6
  57. package/dist/apps/data/routes/funding.js.map +1 -1
  58. package/dist/apps/data/routes/indexer.d.ts +5 -5
  59. package/dist/apps/data/routes/receipts.d.ts +392 -315
  60. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  61. package/dist/apps/data/routes/receipts.js +152 -77
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/tokens.d.ts +166 -173
  64. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  65. package/dist/apps/data/routes/tokens.js +14 -12
  66. package/dist/apps/data/routes/tokens.js.map +1 -1
  67. package/dist/apps/data/routes/transactions.d.ts +277 -536
  68. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +96 -77
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts +82 -17
  72. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  73. package/dist/apps/data/routes/transfers.js +70 -13
  74. package/dist/apps/data/routes/transfers.js.map +1 -1
  75. package/dist/apps/data/routes/valuation.d.ts +307 -0
  76. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  77. package/dist/apps/data/routes/valuation.js +292 -0
  78. package/dist/apps/data/routes/valuation.js.map +1 -0
  79. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.js +7 -5
  81. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  82. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +124 -25
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/index.d.ts +1 -1
  87. package/dist/apps/index.d.ts.map +1 -1
  88. package/dist/apps/index.js +1 -1
  89. package/dist/apps/index.js.map +1 -1
  90. package/dist/apps/management/App.d.ts +8606 -1681
  91. package/dist/apps/management/App.d.ts.map +1 -1
  92. package/dist/apps/management/App.js +12 -1
  93. package/dist/apps/management/App.js.map +1 -1
  94. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  95. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  96. package/dist/apps/management/routes/api-keys.js +5 -3
  97. package/dist/apps/management/routes/api-keys.js.map +1 -1
  98. package/dist/apps/management/routes/billing.d.ts +88 -48
  99. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  100. package/dist/apps/management/routes/billing.js +10 -2
  101. package/dist/apps/management/routes/billing.js.map +1 -1
  102. package/dist/apps/management/routes/faucet.d.ts +218 -0
  103. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  104. package/dist/apps/management/routes/faucet.js +139 -0
  105. package/dist/apps/management/routes/faucet.js.map +1 -0
  106. package/dist/apps/management/routes/invitations.d.ts +121 -121
  107. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  108. package/dist/apps/management/routes/invitations.js +3 -0
  109. package/dist/apps/management/routes/invitations.js.map +1 -1
  110. package/dist/apps/management/routes/invite-links.d.ts +197 -168
  111. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  112. package/dist/apps/management/routes/invite-links.js +26 -1
  113. package/dist/apps/management/routes/invite-links.js.map +1 -1
  114. package/dist/apps/management/routes/me.d.ts +2 -0
  115. package/dist/apps/management/routes/me.d.ts.map +1 -1
  116. package/dist/apps/management/routes/me.js +4 -0
  117. package/dist/apps/management/routes/me.js.map +1 -1
  118. package/dist/apps/management/routes/members.d.ts +49 -49
  119. package/dist/apps/management/routes/orgs.d.ts +91 -91
  120. package/dist/apps/management/routes/projects.d.ts +115 -115
  121. package/dist/apps/management/routes/usage.d.ts +35 -32
  122. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  123. package/dist/apps/management/routes/usage.js +4 -0
  124. package/dist/apps/management/routes/usage.js.map +1 -1
  125. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  126. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  127. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  128. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  129. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  130. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  131. package/dist/apps/management/routes/webhooks.js +573 -0
  132. package/dist/apps/management/routes/webhooks.js.map +1 -0
  133. package/dist/apps/mcp.d.ts +9 -2
  134. package/dist/apps/mcp.d.ts.map +1 -1
  135. package/dist/apps/mcp.js +9 -4
  136. package/dist/apps/mcp.js.map +1 -1
  137. package/dist/cli/Cli.d.ts.map +1 -1
  138. package/dist/cli/Cli.js +7 -2
  139. package/dist/cli/Cli.js.map +1 -1
  140. package/dist/db/Db.d.ts +4 -0
  141. package/dist/db/Db.d.ts.map +1 -1
  142. package/dist/db/Db.js +83 -0
  143. package/dist/db/Db.js.map +1 -1
  144. package/dist/db/Schema.d.ts +41 -0
  145. package/dist/db/Schema.d.ts.map +1 -1
  146. package/dist/db/Schema.js +45 -0
  147. package/dist/db/Schema.js.map +1 -1
  148. package/dist/db/tables/earlyAccess.d.ts +50 -0
  149. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  150. package/dist/db/tables/earlyAccess.js +63 -0
  151. package/dist/db/tables/earlyAccess.js.map +1 -0
  152. package/dist/db/tables/invitations.d.ts +12 -1
  153. package/dist/db/tables/invitations.d.ts.map +1 -1
  154. package/dist/db/tables/invitations.js +23 -0
  155. package/dist/db/tables/invitations.js.map +1 -1
  156. package/dist/db/tables/inviteLinks.d.ts +10 -2
  157. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  158. package/dist/db/tables/inviteLinks.js +10 -1
  159. package/dist/db/tables/inviteLinks.js.map +1 -1
  160. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  161. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.js +2 -0
  163. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  164. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  165. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  166. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  167. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  168. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  169. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  170. package/dist/db/tables/webhookSubscriptions.js +16 -0
  171. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  172. package/dist/internal/Auth.d.ts +43 -2
  173. package/dist/internal/Auth.d.ts.map +1 -1
  174. package/dist/internal/Auth.js +60 -45
  175. package/dist/internal/Auth.js.map +1 -1
  176. package/dist/internal/FundingQuotes.d.ts +69 -15
  177. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  178. package/dist/internal/FundingQuotes.js +74 -25
  179. package/dist/internal/FundingQuotes.js.map +1 -1
  180. package/dist/internal/Log.d.ts +19 -1
  181. package/dist/internal/Log.d.ts.map +1 -1
  182. package/dist/internal/Log.js +2 -0
  183. package/dist/internal/Log.js.map +1 -1
  184. package/dist/internal/MetricSink.d.ts.map +1 -1
  185. package/dist/internal/MetricSink.js +9 -0
  186. package/dist/internal/MetricSink.js.map +1 -1
  187. package/dist/internal/OpenApi.d.ts +2 -2
  188. package/dist/internal/OpenApi.d.ts.map +1 -1
  189. package/dist/internal/OpenApi.js +3 -0
  190. package/dist/internal/OpenApi.js.map +1 -1
  191. package/dist/internal/Schema.d.ts +14 -5
  192. package/dist/internal/Schema.d.ts.map +1 -1
  193. package/dist/internal/Schema.js +23 -5
  194. package/dist/internal/Schema.js.map +1 -1
  195. package/dist/internal/Value.d.ts +18 -0
  196. package/dist/internal/Value.d.ts.map +1 -1
  197. package/dist/internal/Value.js +11 -1
  198. package/dist/internal/Value.js.map +1 -1
  199. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  200. package/dist/internal/VerifiedTokens.js +10 -1
  201. package/dist/internal/VerifiedTokens.js.map +1 -1
  202. package/dist/internal/Viem.d.ts +1 -1
  203. package/dist/internal/Webhooks.d.ts +24 -3
  204. package/dist/internal/Webhooks.d.ts.map +1 -1
  205. package/dist/internal/Webhooks.js +34 -8
  206. package/dist/internal/Webhooks.js.map +1 -1
  207. package/package.json +2 -2
  208. package/src/App.ts +9 -1
  209. package/src/Client.test-d.ts +638 -6
  210. package/src/admin/App.ts +4 -0
  211. package/src/admin/apps/early-access.test.ts +125 -0
  212. package/src/admin/apps/early-access.ts +175 -0
  213. package/src/admin/apps/verified-tokens.test.ts +201 -0
  214. package/src/admin/apps/verified-tokens.ts +532 -286
  215. package/src/admin/ui/plugin.ts +2 -1
  216. package/src/admin/ui/src/lib/early-access.ts +63 -0
  217. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  218. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  219. package/src/admin/ui/src/routes/__root.tsx +2 -1
  220. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  221. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  222. package/src/admin/ui.generated.ts +48 -33
  223. package/src/apps/data/App.ts +22 -14
  224. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  225. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  226. package/src/apps/data/FxOracle.test.ts +118 -0
  227. package/src/apps/data/FxOracle.ts +153 -0
  228. package/src/apps/data/index.ts +1 -0
  229. package/src/apps/data/routes/activities.test.ts +173 -3
  230. package/src/apps/data/routes/activities.ts +403 -132
  231. package/src/apps/data/routes/addresses.ts +19 -9
  232. package/src/apps/data/routes/balances.test.ts +144 -11
  233. package/src/apps/data/routes/balances.ts +140 -51
  234. package/src/apps/data/routes/exchanges.test.ts +540 -9
  235. package/src/apps/data/routes/exchanges.ts +690 -103
  236. package/src/apps/data/routes/funding.test.ts +84 -0
  237. package/src/apps/data/routes/funding.ts +38 -6
  238. package/src/apps/data/routes/receipts.test.ts +125 -23
  239. package/src/apps/data/routes/receipts.ts +205 -100
  240. package/src/apps/data/routes/tokens.test.ts +43 -0
  241. package/src/apps/data/routes/tokens.ts +27 -12
  242. package/src/apps/data/routes/transactions.test.ts +72 -19
  243. package/src/apps/data/routes/transactions.ts +162 -92
  244. package/src/apps/data/routes/transfers.test.ts +146 -4
  245. package/src/apps/data/routes/transfers.ts +91 -13
  246. package/src/apps/data/routes/valuation.test.ts +422 -0
  247. package/src/apps/data/routes/valuation.ts +423 -0
  248. package/src/apps/data/routes/verified-tokens.ts +14 -2
  249. package/src/apps/data/routes/webhooks.test.ts +56 -8
  250. package/src/apps/data/routes/webhooks.ts +144 -27
  251. package/src/apps/index.ts +1 -1
  252. package/src/apps/management/App.ts +13 -0
  253. package/src/apps/management/routes/api-keys.test.ts +87 -0
  254. package/src/apps/management/routes/api-keys.ts +4 -3
  255. package/src/apps/management/routes/billing.test.ts +112 -1
  256. package/src/apps/management/routes/billing.ts +7 -2
  257. package/src/apps/management/routes/faucet.test.ts +79 -0
  258. package/src/apps/management/routes/faucet.ts +185 -0
  259. package/src/apps/management/routes/invitations.test.ts +172 -0
  260. package/src/apps/management/routes/invitations.ts +3 -0
  261. package/src/apps/management/routes/invite-links.test.ts +117 -0
  262. package/src/apps/management/routes/invite-links.ts +43 -1
  263. package/src/apps/management/routes/me.test.ts +140 -88
  264. package/src/apps/management/routes/me.ts +6 -0
  265. package/src/apps/management/routes/usage.test.ts +6 -0
  266. package/src/apps/management/routes/usage.ts +7 -0
  267. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  268. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  269. package/src/apps/management/routes/webhooks.test.ts +426 -0
  270. package/src/apps/management/routes/webhooks.ts +672 -0
  271. package/src/apps/mcp.test.ts +13 -0
  272. package/src/apps/mcp.ts +18 -5
  273. package/src/cli/Cli.test.ts +36 -0
  274. package/src/cli/Cli.ts +7 -2
  275. package/src/db/Db.test.ts +19 -0
  276. package/src/db/Db.ts +95 -0
  277. package/src/db/Schema.ts +53 -0
  278. package/src/db/tables/earlyAccess.ts +86 -0
  279. package/src/db/tables/invitations.ts +26 -1
  280. package/src/db/tables/inviteLinks.ts +22 -3
  281. package/src/db/tables/sponsoredTransactions.ts +4 -0
  282. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  283. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  284. package/src/db/tables/webhookSubscriptions.ts +31 -0
  285. package/src/internal/Auth.ts +73 -45
  286. package/src/internal/FundingQuotes.test.ts +143 -1
  287. package/src/internal/FundingQuotes.ts +95 -28
  288. package/src/internal/Log.test-d.ts +21 -0
  289. package/src/internal/Log.test.ts +50 -0
  290. package/src/internal/Log.ts +24 -1
  291. package/src/internal/MetricSink.test.ts +160 -0
  292. package/src/internal/MetricSink.ts +10 -0
  293. package/src/internal/OpenApi.ts +3 -0
  294. package/src/internal/Schema.ts +50 -5
  295. package/src/internal/Value.test.ts +20 -0
  296. package/src/internal/Value.ts +24 -1
  297. package/src/internal/VerifiedTokens.ts +9 -1
  298. package/src/internal/Webhooks.test.ts +140 -0
  299. package/src/internal/Webhooks.ts +53 -9
package/src/apps/mcp.ts CHANGED
@@ -15,7 +15,7 @@ const excludedTags = ['CoinGecko', 'MCP']
15
15
 
16
16
  /** Usage guidance surfaced to MCP clients on initialize. */
17
17
  const instructions =
18
- "Tools mirror the Tempo API's data domain. Authenticated tools accept a Tempo API key sent as an `Authorization: Bearer` header on the HTTP request. List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation."
18
+ "Tempo MCP provides access to the Tempo API's data domain and documentation. Authenticated tools accept a Tempo API key sent as an `Authorization: Bearer` header on the HTTP request. List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation."
19
19
 
20
20
  /** Zod schemas owned by the MCP endpoint's OpenAPI description. */
21
21
  export namespace schema {
@@ -61,11 +61,22 @@ export namespace schema {
61
61
  * the other route groups:
62
62
  *
63
63
  * ```ts
64
- * App.create(options).route('/', mcp({ fetch: (request) => getApp().fetch(request) }))
64
+ * const app = App.create(options)
65
+ * const fetch = app.fetch.bind(app)
66
+ * app.route('/', mcp({ fetch }))
65
67
  * ```
68
+ *
69
+ * Capture `fetch` before adding request-level wrappers, so nested calls stay
70
+ * within the outer request's instrumentation lifecycle.
66
71
  */
67
72
  export function mcp(options: mcp.Options) {
68
- const { docs = 'https://mcp.tempo.xyz/mcp', fetch, name = 'tapimo', version = '0.0.0' } = options
73
+ const {
74
+ docs = 'https://mcp.tempo.xyz/mcp',
75
+ fetch,
76
+ name = 'tempo',
77
+ title = 'Tempo MCP',
78
+ version = '0.0.0',
79
+ } = options
69
80
 
70
81
  // One incur instance per caller origin: the OpenAPI document is parsed once,
71
82
  // and generated commands rebase onto the origin so tool results embed real
@@ -95,7 +106,7 @@ export function mcp(options: mcp.Options) {
95
106
  const url = new URL(request.url)
96
107
  return fetch(new Request(new URL(url.pathname + url.search, origin), request))
97
108
  },
98
- mcp: { instructions },
109
+ mcp: { instructions, title },
99
110
  openapi: scope(document),
100
111
  // `compact` + `security: false` keep tool schemas lean: no examples,
101
112
  // oversized regexes, or per-tool credential inputs (auth rides the
@@ -271,8 +282,10 @@ export declare namespace mcp {
271
282
  docs?: Source | false | undefined
272
283
  /** In-process fetch handler for the composed app (e.g. `(request) => app.fetch(request)`). */
273
284
  fetch: (request: Request) => Response | Promise<Response>
274
- /** MCP server name. @default 'tapimo' */
285
+ /** MCP server name. @default 'tempo' */
275
286
  name?: string | undefined
287
+ /** Human-readable MCP server title. @default 'Tempo MCP' */
288
+ title?: string | undefined
276
289
  /** MCP server version. @default '0.0.0' */
277
290
  version?: string | undefined
278
291
  }
@@ -16,6 +16,14 @@ describe('from', () => {
16
16
 
17
17
  expect(cli.name).toMatchInlineSnapshot(`"tapimo"`)
18
18
  expect(cli.description).toMatchInlineSnapshot(`"Tempo API CLI"`)
19
+ expect((await initialize(cli)).result).toMatchObject({
20
+ instructions: expect.stringContaining('Tempo MCP provides access'),
21
+ serverInfo: {
22
+ name: 'tempo',
23
+ title: 'Tempo MCP',
24
+ version: '1.2.3',
25
+ },
26
+ })
19
27
  } finally {
20
28
  fetch.mockRestore()
21
29
  }
@@ -91,6 +99,34 @@ function mockHostedApi(app = TestApp.create()) {
91
99
  })
92
100
  }
93
101
 
102
+ async function initialize(cli: ReturnType<typeof Cli.from>) {
103
+ const response = await cli.fetch(
104
+ new Request('http://localhost/mcp', {
105
+ body: JSON.stringify({
106
+ id: 1,
107
+ jsonrpc: '2.0',
108
+ method: 'initialize',
109
+ params: {
110
+ capabilities: {},
111
+ clientInfo: { name: 'test-client', version: '1.0.0' },
112
+ protocolVersion: '2025-03-26',
113
+ },
114
+ }),
115
+ headers: {
116
+ accept: 'application/json, text/event-stream',
117
+ 'content-type': 'application/json',
118
+ },
119
+ method: 'POST',
120
+ }),
121
+ )
122
+ return (await response.json()) as {
123
+ result: {
124
+ instructions: string
125
+ serverInfo: { name: string; title: string; version: string }
126
+ }
127
+ }
128
+ }
129
+
94
130
  async function serve(cli: ReturnType<typeof Cli.from>, argv: string[]) {
95
131
  const output: string[] = []
96
132
  await cli.serve(argv, {
package/src/cli/Cli.ts CHANGED
@@ -8,7 +8,7 @@ const excludedTools = ['admin_*', 'gecko_*', 'v1_auth_*']
8
8
 
9
9
  /** Usage guidance surfaced to MCP clients on initialize (`<cli> --mcp`). */
10
10
  const instructions =
11
- 'Tools mirror the hosted Tempo API. Authenticated tools accept an `authorization` input (`Bearer <api key>`). List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation.'
11
+ 'Tempo MCP provides access to the hosted Tempo API and documentation. Authenticated tools accept an `authorization` input (`Bearer <api key>`). List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation.'
12
12
 
13
13
  /**
14
14
  * Creates an incur CLI backed by a hosted Tempo API, plus the super admin
@@ -28,7 +28,12 @@ export function from(url: string | URL, options: from.Options = {}) {
28
28
  .create(name, {
29
29
  description,
30
30
  fetch: Fetch.fromRequest(url, request),
31
- mcp: { instructions, tools: { exclude: excludedTools } },
31
+ mcp: {
32
+ instructions,
33
+ name: 'tempo',
34
+ title: 'Tempo MCP',
35
+ tools: { exclude: excludedTools },
36
+ },
32
37
  openapi,
33
38
  openapiConfig: options.openapiConfig ?? { mode: 'namespace' },
34
39
  version,
package/src/db/Db.test.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { sql } from 'kysely'
1
2
  import { nanoid } from 'nanoid'
2
3
 
3
4
  import * as Runtime from '../../test/runtime.js'
4
5
  import * as Db from './Db.js'
6
+ import * as Invitations from './tables/invitations.js'
5
7
  import * as Organizations from './tables/organizations.js'
6
8
 
7
9
  const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
@@ -15,6 +17,23 @@ describe('postgres', () => {
15
17
  await db.close()
16
18
  })
17
19
 
20
+ test('behavior: legacy invitation inserts retain early access during cutover', async () => {
21
+ const db = create()
22
+ await db.migrate()
23
+ await Organizations.create(db, { id: 'org_legacy', name: 'Legacy' })
24
+ await sql`
25
+ INSERT INTO invitations (
26
+ id, org_id, email, role, invited_by, expires_at, accepted_at, revoked_at, created_at
27
+ ) VALUES (
28
+ 'inv_legacy', 'org_legacy', 'legacy@example.org', 'member', 'usr_legacy',
29
+ '2027-01-01T00:00:00.000Z', NULL, NULL, '2026-07-20T00:00:00.000Z'
30
+ )
31
+ `.execute(db.kysely)
32
+
33
+ expect((await Invitations.get(db, 'inv_legacy'))?.grantsEarlyAccess).toBe(true)
34
+ await db.close()
35
+ })
36
+
18
37
  test('behavior: creates a Postgres db without schema scoping', async () => {
19
38
  const db = Db.postgres({ connectionString: Runtime.postgresUrl })
20
39
  await db.close()
package/src/db/Db.ts CHANGED
@@ -12,6 +12,7 @@ import * as pg from 'pg'
12
12
 
13
13
  import type * as AdminAuditLogs from './tables/adminAuditLogs.js'
14
14
  import type * as BillingSettings from './tables/billingSettings.js'
15
+ import type * as EarlyAccess from './tables/earlyAccess.js'
15
16
  import type * as Invitations from './tables/invitations.js'
16
17
  import type * as InviteLinks from './tables/inviteLinks.js'
17
18
  import type * as Memberships from './tables/memberships.js'
@@ -21,6 +22,7 @@ import type * as RequestUsage from './tables/requestUsage.js'
21
22
  import type * as SponsoredTransactions from './tables/sponsoredTransactions.js'
22
23
  import type * as StripeCustomers from './tables/stripeCustomers.js'
23
24
  import type * as Users from './tables/users.js'
25
+ import type * as VerifiedTokenRequests from './tables/verifiedTokenRequests.js'
24
26
  import type * as VerifiedTokens from './tables/verifiedTokens.js'
25
27
  import type * as WebhookDeliveries from './tables/webhookDeliveries.js'
26
28
  import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
@@ -32,6 +34,7 @@ import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
32
34
  export type Database = {
33
35
  admin_audit_logs: AdminAuditLogs.Table
34
36
  billing_settings: BillingSettings.Table
37
+ early_access: EarlyAccess.Table
35
38
  invitations: Invitations.Table
36
39
  invite_link_redemptions: InviteLinks.RedemptionTable
37
40
  invite_links: InviteLinks.Table
@@ -51,6 +54,7 @@ export type Database = {
51
54
  stripe_customers: StripeCustomers.Table
52
55
  users: Users.Table
53
56
  verified_token_lists: VerifiedTokens.ListTable
57
+ verified_token_requests: VerifiedTokenRequests.Table
54
58
  verified_tokens: VerifiedTokens.Table
55
59
  webhook_deliveries: WebhookDeliveries.Table
56
60
  webhook_subscriptions: WebhookSubscriptions.Table
@@ -645,6 +649,97 @@ export const migrations: readonly Migration[] = [
645
649
  name: '0058_request_usage_sequence_bigint',
646
650
  up: (db) => raw(db, sql`ALTER TABLE request_usage_meter_events ALTER COLUMN sequence TYPE bigint`), // prettier-ignore
647
651
  },
652
+ {
653
+ name: '0059_verified_token_requests',
654
+ up: (db) =>
655
+ db.schema
656
+ .createTable('verifiedTokenRequests')
657
+ .ifNotExists()
658
+ .addColumn('id', 'text', (c) => c.primaryKey())
659
+ .addColumn('orgId', 'text', (c) => c.notNull())
660
+ .addColumn('requestedBy', 'text', (c) => c.notNull())
661
+ .addColumn('chainId', 'integer', (c) => c.notNull())
662
+ .addColumn('address', 'text', (c) => c.notNull())
663
+ .addColumn('symbol', 'text', (c) => c.notNull())
664
+ .addColumn('name', 'text', (c) => c.notNull())
665
+ .addColumn('currency', 'text', (c) => c.notNull())
666
+ .addColumn('decimals', 'integer', (c) => c.notNull())
667
+ .addColumn('logoUri', 'text')
668
+ .addColumn('note', 'text')
669
+ .addColumn('status', 'text', (c) => c.notNull().defaultTo('pending'))
670
+ .addColumn('reviewNote', 'text')
671
+ .addColumn('reviewedBy', 'text')
672
+ .addColumn('reviewedAt', 'text')
673
+ .addColumn('createdAt', 'text', (c) => c.notNull())
674
+ .addColumn('updatedAt', 'text', (c) => c.notNull()),
675
+ },
676
+ {
677
+ name: '0060_verified_token_requests_org_index',
678
+ up: (db) =>
679
+ db.schema
680
+ .createIndex('verified_token_requests_org_idx')
681
+ .ifNotExists()
682
+ .on('verifiedTokenRequests')
683
+ .columns(['orgId', 'createdAt']),
684
+ },
685
+ {
686
+ name: '0061_verified_token_requests_pending_unique',
687
+ // Raw SQL: a partial unique index (snake_case; the CamelCase plugin does
688
+ // not map raw statements). At most one live pending request per
689
+ // (chain, address) globally; addresses are stored lowercase.
690
+ up: (db) =>
691
+ raw(
692
+ db,
693
+ sql`CREATE UNIQUE INDEX IF NOT EXISTS verified_token_requests_pending_unique ON verified_token_requests (chain_id, address) WHERE status = 'pending'`,
694
+ ),
695
+ },
696
+ {
697
+ name: '0062_verified_token_requests_status_index',
698
+ up: (db) =>
699
+ db.schema
700
+ .createIndex('verified_token_requests_status_idx')
701
+ .ifNotExists()
702
+ .on('verifiedTokenRequests')
703
+ .columns(['status', 'createdAt']),
704
+ },
705
+ {
706
+ name: '0063_verified_token_requests_logo',
707
+ up: (db) => raw(db, sql`ALTER TABLE verified_token_requests ADD COLUMN IF NOT EXISTS logo text`), // prettier-ignore
708
+ },
709
+ {
710
+ name: '0064_invitations_grants_early_access',
711
+ up: (db) => raw(db, sql`ALTER TABLE invitations ADD COLUMN IF NOT EXISTS grants_early_access boolean NOT NULL DEFAULT false`), // prettier-ignore
712
+ },
713
+ {
714
+ name: '0065_invitations_grants_early_access_backfill',
715
+ // Pre-gate invitations came from trusted-era users; the fixed cutoff keeps
716
+ // re-runs (test DBs replay every migration) from flipping newer rows.
717
+ up: (db) => raw(db, sql`UPDATE invitations SET grants_early_access = true WHERE created_at < '2026-07-20T00:00:00.000Z'`), // prettier-ignore
718
+ },
719
+ {
720
+ name: '0066_early_access',
721
+ up: (db) =>
722
+ db.schema
723
+ .createTable('earlyAccess')
724
+ .ifNotExists()
725
+ .addColumn('entry', 'text', (c) => c.primaryKey())
726
+ .addColumn('createdAt', 'text', (c) => c.notNull())
727
+ .addColumn('createdBy', 'text', (c) => c.notNull()),
728
+ },
729
+ {
730
+ name: '0067_invitations_grants_early_access_cutover',
731
+ // Every invitation present at deployment came from a trusted-era user.
732
+ up: (db) => raw(db, sql`UPDATE invitations SET grants_early_access = true`),
733
+ },
734
+ {
735
+ name: '0068_invitations_grants_early_access_legacy_default',
736
+ // Old workers omit this column during deploy; new writes set it explicitly.
737
+ up: (db) => raw(db, sql`ALTER TABLE invitations ALTER COLUMN grants_early_access SET DEFAULT true`), // prettier-ignore
738
+ },
739
+ {
740
+ name: '0069_invite_links_allowed_email_domains',
741
+ up: (db) => raw(db, sql`ALTER TABLE invite_links ADD COLUMN IF NOT EXISTS allowed_email_domains jsonb`), // prettier-ignore
742
+ },
648
743
  ]
649
744
 
650
745
  /**
package/src/db/Schema.ts CHANGED
@@ -52,6 +52,20 @@ export const BillingSettings = z.object({
52
52
  /** Columns of the `billing_settings` table. */
53
53
  export type BillingSettings = z.output<typeof BillingSettings>
54
54
 
55
+ /** An early-access allowlist entry: a lowercase domain or exact email. */
56
+ export const EarlyAccessEntry = z.object({
57
+ createdAt: z.string().check(z.describe('When the entry was added (ISO 8601).')),
58
+ createdBy: z
59
+ .string()
60
+ .check(z.describe('Admin email that added the entry, or `migration` for seeded rows.')),
61
+ entry: z
62
+ .string()
63
+ .check(z.describe('Lowercase domain or exact email (contains `@`) granted early access.')),
64
+ })
65
+
66
+ /** Columns of the `early_access` table. */
67
+ export type EarlyAccessEntry = z.output<typeof EarlyAccessEntry>
68
+
55
69
  /** A pending, expiring offer of organization membership. */
56
70
  export const Invitation = z.object({
57
71
  acceptedAt: z
@@ -62,6 +76,9 @@ export const Invitation = z.object({
62
76
  .string()
63
77
  .check(z.describe('Invitee email (stored lowercase); matched against verified emails.')),
64
78
  expiresAt: z.string().check(z.describe('When the invitation expires (ISO 8601).')),
79
+ grantsEarlyAccess: z
80
+ .boolean()
81
+ .check(z.describe("Whether the invitation grants early access; stamped from the inviter's own access at creation.")), // prettier-ignore
65
82
  id: z
66
83
  .string()
67
84
  .check(
@@ -83,6 +100,9 @@ export type Invitation = z.output<typeof Invitation>
83
100
 
84
101
  /** A reusable bearer link granting organization membership. */
85
102
  export const InviteLink = z.object({
103
+ allowedEmailDomains: z
104
+ .nullable(z.readonly(z.array(z.string())))
105
+ .check(z.describe('Lowercase email domains allowed to redeem, or null for unrestricted.')),
86
106
  createdAt: z.string().check(z.describe('When the link was created (ISO 8601).')),
87
107
  createdBy: z.string().check(z.describe('User id that created the link, or `super_admin`.')),
88
108
  deletedAt: z.nullable(z.string()).check(z.describe('When the link was soft deleted, or null.')),
@@ -363,6 +383,39 @@ export const VerifiedTokenList = z.object({
363
383
  /** Columns of the `verified_token_lists` table. */
364
384
  export type VerifiedTokenList = z.output<typeof VerifiedTokenList>
365
385
 
386
+ /**
387
+ * An organization's request to add a token to the curated verified list.
388
+ * Addresses are stored lowercase; a partial unique index allows at most one
389
+ * pending request per (chain, address) globally.
390
+ */
391
+ export const VerifiedTokenRequest = z.object({
392
+ address: z.string().check(z.describe('Requested TIP-20 token contract address (lowercase).')),
393
+ chainId: z.number().check(z.int(), z.describe('Chain the token belongs to.')),
394
+ createdAt: z.string().check(z.describe('When the request was created (ISO 8601).')),
395
+ currency: z.string().check(z.describe('Display currency, e.g. `USD`.')),
396
+ decimals: z.number().check(z.int(), z.describe('Decimal precision.')),
397
+ id: z.string().check(z.describe('Opaque request id (`vtr_…`).')),
398
+ logo: z
399
+ .nullable(z.string())
400
+ .check(z.describe('Inline SVG logo markup uploaded by the requester, or null.')),
401
+ logoUri: z.nullable(z.string()).check(z.describe('HTTPS logo URL from chain metadata, or null.')),
402
+ name: z.string().check(z.describe('Display name.')),
403
+ note: z.nullable(z.string()).check(z.describe('Requester note to reviewers, or null.')),
404
+ orgId: z.string().check(z.describe('Requesting organization id (`org_…`).')),
405
+ requestedBy: z
406
+ .string()
407
+ .check(z.describe('User id that submitted the request, or `super_admin`.')),
408
+ reviewNote: z.nullable(z.string()).check(z.describe('Reviewer note shown on denial, or null.')),
409
+ reviewedAt: z.nullable(z.string()).check(z.describe('When the request was reviewed, or null.')),
410
+ reviewedBy: z.nullable(z.string()).check(z.describe('Reviewing admin email, or null.')),
411
+ status: z.enum(['approved', 'denied', 'pending']).check(z.describe('Review status.')),
412
+ symbol: z.string().check(z.describe('Ticker symbol.'), z.meta({ examples: ['USDC'] })),
413
+ updatedAt: z.string().check(z.describe('When the request was last changed (ISO 8601).')),
414
+ })
415
+
416
+ /** Columns of the `verified_token_requests` table. */
417
+ export type VerifiedTokenRequest = z.output<typeof VerifiedTokenRequest>
418
+
366
419
  /**
367
420
  * A webhook subscription. The `Webhooks.Owner` union maps to (`ownerType`,
368
421
  * `ownerId`); jsonb object columns round-trip as parsed values. `pollerCursor`
@@ -0,0 +1,86 @@
1
+ import type { Selectable } from 'kysely'
2
+
3
+ import type * as Db from '../Db.js'
4
+ import type * as db_Schema from '../Schema.js'
5
+
6
+ /** Columns of the `early_access` table, derived from `Schema.EarlyAccessEntry`. */
7
+ export type Table = db_Schema.EarlyAccessEntry
8
+
9
+ /** A stored early-access allowlist row. */
10
+ export type Record = Selectable<Table>
11
+
12
+ /**
13
+ * Inserts an allowlist entry (a domain or exact email, normalized lowercase).
14
+ * Returns the stored record, or `undefined` when the entry already exists.
15
+ *
16
+ * @param db - The database.
17
+ * @param input - The entry to insert.
18
+ * @returns The stored record, or `undefined` when already present.
19
+ */
20
+ export function add(db: Db.Db, input: add.Input): Promise<Record | undefined> {
21
+ return db.kysely
22
+ .insertInto('early_access')
23
+ .values({
24
+ createdAt: new Date().toISOString(),
25
+ createdBy: input.createdBy,
26
+ entry: input.entry.trim().toLowerCase(),
27
+ })
28
+ .onConflict((oc) => oc.column('entry').doNothing())
29
+ .returningAll()
30
+ .executeTakeFirst()
31
+ }
32
+
33
+ export declare namespace add {
34
+ /** Fields accepted when inserting an allowlist entry. */
35
+ type Input = {
36
+ /** Admin email adding the entry, or `migration` for seeded rows. */
37
+ createdBy: string
38
+ /** Domain or exact email to grant early access; normalized lowercase. */
39
+ entry: string
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Lists all allowlist entries, ordered by entry.
45
+ *
46
+ * @param db - The database.
47
+ * @returns The records.
48
+ */
49
+ export function list(db: Db.Db): Promise<Record[]> {
50
+ return db.kysely.selectFrom('early_access').selectAll().orderBy('entry', 'asc').execute()
51
+ }
52
+
53
+ /**
54
+ * Removes an allowlist entry.
55
+ *
56
+ * @param db - The database.
57
+ * @param entry - The entry to remove (matched lowercase).
58
+ * @returns The removed record, or `undefined` when absent.
59
+ */
60
+ export function remove(db: Db.Db, entry: string): Promise<Record | undefined> {
61
+ return db.kysely
62
+ .deleteFrom('early_access')
63
+ .where('entry', '=', entry.trim().toLowerCase())
64
+ .returningAll()
65
+ .executeTakeFirst()
66
+ }
67
+
68
+ /**
69
+ * Whether an allowlist entry matches `email`: the exact email or its domain.
70
+ * Two primary-key lookups via `IN`.
71
+ *
72
+ * @param db - The database.
73
+ * @param email - The email to match (case-insensitive).
74
+ * @returns Whether an entry matches.
75
+ */
76
+ export async function matches(db: Db.Db, email: string): Promise<boolean> {
77
+ const normalized = email.trim().toLowerCase()
78
+ const domain = normalized.slice(normalized.lastIndexOf('@') + 1)
79
+ const row = await db.kysely
80
+ .selectFrom('early_access')
81
+ .select('entry')
82
+ .where('entry', 'in', [normalized, domain])
83
+ .limit(1)
84
+ .executeTakeFirst()
85
+ return row !== undefined
86
+ }
@@ -1,4 +1,4 @@
1
- import type { Selectable } from 'kysely'
1
+ import { type Selectable, sql } from 'kysely'
2
2
 
3
3
  import * as Id from '../../internal/Id.js'
4
4
  import type * as Db from '../Db.js'
@@ -31,6 +31,7 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
31
31
  createdAt: now,
32
32
  email: input.email.toLowerCase(),
33
33
  expiresAt: input.expiresAt,
34
+ grantsEarlyAccess: input.grantsEarlyAccess,
34
35
  id: Id.generate('inv'),
35
36
  invitedBy: input.invitedBy,
36
37
  orgId: input.orgId,
@@ -45,6 +46,8 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
45
46
  .doUpdateSet({
46
47
  createdAt: now,
47
48
  expiresAt: input.expiresAt,
49
+ // Re-invites upgrade the grant, never withdraw it (raw SQL stays snake_case).
50
+ grantsEarlyAccess: sql<boolean>`invitations.grants_early_access OR excluded.grants_early_access`,
48
51
  invitedBy: input.invitedBy,
49
52
  role: input.role,
50
53
  }),
@@ -60,6 +63,8 @@ export declare namespace create {
60
63
  email: string
61
64
  /** When the invitation expires (ISO 8601). */
62
65
  expiresAt: string
66
+ /** Whether accepting grants early access; stamped from the inviter's own access. */
67
+ grantsEarlyAccess: boolean
63
68
  /** User or API key id creating the invitation. */
64
69
  invitedBy: string
65
70
  /** Organization id (`org_…`) the invitation joins. */
@@ -114,6 +119,26 @@ export function listByEmail(db: Db.Db, email: string): Promise<(Record & { orgNa
114
119
  .execute()
115
120
  }
116
121
 
122
+ /**
123
+ * Whether any unrevoked invitation grants `email` early access. Acceptance and
124
+ * expiry are deliberately ignored: a trusted invite grants access until revoked.
125
+ *
126
+ * @param db - The database.
127
+ * @param email - The invitee email (matched lowercase).
128
+ * @returns Whether a grant exists.
129
+ */
130
+ export async function hasEarlyAccessGrant(db: Db.Db, email: string): Promise<boolean> {
131
+ const row = await db.kysely
132
+ .selectFrom('invitations')
133
+ .select('id')
134
+ .where('email', '=', email.toLowerCase())
135
+ .where('grantsEarlyAccess', '=', true)
136
+ .where('revokedAt', 'is', null)
137
+ .limit(1)
138
+ .executeTakeFirst()
139
+ return row !== undefined
140
+ }
141
+
117
142
  /**
118
143
  * Revokes a pending invitation.
119
144
  *
@@ -1,11 +1,18 @@
1
- import type { Selectable } from 'kysely'
1
+ import type { JSONColumnType, Selectable } from 'kysely'
2
2
 
3
3
  import * as Id from '../../internal/Id.js'
4
4
  import type * as Db from '../Db.js'
5
5
  import type * as db_Schema from '../Schema.js'
6
6
 
7
7
  /** Columns of the `invite_links` table. */
8
- export type Table = db_Schema.InviteLink
8
+ export type Table = Omit<db_Schema.InviteLink, 'allowedEmailDomains'> & {
9
+ /** Lowercase email domains allowed to redeem, or null for unrestricted. */
10
+ allowedEmailDomains: JSONColumnType<
11
+ db_Schema.InviteLink['allowedEmailDomains'],
12
+ string | null,
13
+ string | null
14
+ >
15
+ }
9
16
  /** Columns of the `invite_link_redemptions` table. */
10
17
  export type RedemptionTable = db_Schema.InviteLinkRedemption
11
18
  /** A stored invite link. */
@@ -28,6 +35,7 @@ export function status(record: Record, now = new Date().toISOString()): Status {
28
35
  export function create(
29
36
  db: Db.Db,
30
37
  input: {
38
+ allowedEmailDomains: readonly string[] | null
31
39
  createdBy: string
32
40
  expiresAt: string | null
33
41
  maxUses: number | null
@@ -40,6 +48,8 @@ export function create(
40
48
  return db.kysely
41
49
  .insertInto('invite_links')
42
50
  .values({
51
+ allowedEmailDomains:
52
+ input.allowedEmailDomains === null ? null : JSON.stringify(input.allowedEmailDomains),
43
53
  createdAt: now,
44
54
  createdBy: input.createdBy,
45
55
  deletedAt: null,
@@ -155,6 +165,10 @@ export function accept(
155
165
  .executeTakeFirst()
156
166
  if (existing) return { consumed: false, link }
157
167
  if (status(link, now) !== 'active') return undefined
168
+ const normalizedEmail = email.trim().toLowerCase()
169
+ const domain = normalizedEmail.slice(normalizedEmail.lastIndexOf('@') + 1)
170
+ if (link.allowedEmailDomains !== null && !link.allowedEmailDomains.includes(domain))
171
+ throw new EmailDomainForbiddenError()
158
172
  const inserted = await trx
159
173
  .insertInto('memberships')
160
174
  .values({ createdAt: now, orgId: link.orgId, role: 'member', updatedAt: now, userId })
@@ -171,7 +185,7 @@ export function accept(
171
185
  .insertInto('invite_link_redemptions')
172
186
  .values({
173
187
  createdAt: now,
174
- email: email.toLowerCase(),
188
+ email: normalizedEmail,
175
189
  id: Id.generate('ilr'),
176
190
  inviteLinkId: link.id,
177
191
  inviteLinkName: link.name,
@@ -185,3 +199,8 @@ export function accept(
185
199
  }
186
200
  })
187
201
  }
202
+
203
+ /** The verified email domain is not allowed by the invite link. */
204
+ export class EmailDomainForbiddenError extends Error {
205
+ override name = 'InviteLinks.EmailDomainForbiddenError'
206
+ }
@@ -397,6 +397,7 @@ export async function usage(db: Db.Db, options: usage.Options): Promise<usage.Bu
397
397
  .select([
398
398
  sql<string>`to_char(${bucket}, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`.as('timestamp'),
399
399
  sql<string>`count(*)`.as('count'),
400
+ sql<string>`count(*) filter (where status = 'failed')`.as('failed'),
400
401
  sql<string>`coalesce(sum(case when status = 'finalized' then fee_amount::numeric when status = 'pending' then fee_max::numeric else 0 end), 0)`.as('feeTotal'), // prettier-ignore
401
402
  ])
402
403
  .where('orgId', '=', orgId)
@@ -410,6 +411,7 @@ export async function usage(db: Db.Db, options: usage.Options): Promise<usage.Bu
410
411
  const rows = await query.execute()
411
412
  return rows.map((row) => ({
412
413
  count: Number(row.count),
414
+ failed: Number(row.failed),
413
415
  feeTotal: BigInt(row.feeTotal),
414
416
  timestamp: row.timestamp,
415
417
  }))
@@ -420,6 +422,8 @@ export declare namespace usage {
420
422
  type Bucket = {
421
423
  /** Sponsored transactions recorded in the bucket, any status. */
422
424
  count: number
425
+ /** Sponsored transactions in the bucket whose status is `failed`. */
426
+ failed: number
423
427
  /** Committed fees in fee-token base units: finalized fees plus pending caps. */
424
428
  feeTotal: bigint
425
429
  /** Bucket start (ISO 8601), aligned to UTC calendar boundaries. */