From 4b1eca73eb64c62ebdf668eb18d587510066bd9d Mon Sep 17 00:00:00 2001 From: Jay V Date: Tue, 16 Sep 2025 16:16:30 -0400 Subject: [PATCH] ignore: zen --- .../component/workspace/billing-section.tsx | 153 ++++ cloud/app/src/component/workspace/common.tsx | 27 + .../src/component/workspace/key-section.tsx | 181 +++++ .../workspace/monthly-limit-section.tsx | 129 ++++ .../component/workspace/new-user-section.tsx | 97 +++ .../component/workspace/payment-section.tsx | 56 ++ .../src/component/workspace/usage-section.tsx | 66 ++ cloud/app/src/routes/workspace/[id].tsx | 669 +----------------- 8 files changed, 717 insertions(+), 661 deletions(-) create mode 100644 cloud/app/src/component/workspace/billing-section.tsx create mode 100644 cloud/app/src/component/workspace/common.tsx create mode 100644 cloud/app/src/component/workspace/key-section.tsx create mode 100644 cloud/app/src/component/workspace/monthly-limit-section.tsx create mode 100644 cloud/app/src/component/workspace/new-user-section.tsx create mode 100644 cloud/app/src/component/workspace/payment-section.tsx create mode 100644 cloud/app/src/component/workspace/usage-section.tsx diff --git a/cloud/app/src/component/workspace/billing-section.tsx b/cloud/app/src/component/workspace/billing-section.tsx new file mode 100644 index 00000000..4bc4d422 --- /dev/null +++ b/cloud/app/src/component/workspace/billing-section.tsx @@ -0,0 +1,153 @@ +import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router" +import { createEffect, createMemo, createSignal, For, Show } from "solid-js" +import { Billing } from "@opencode/cloud-core/billing.js" +import { withActor } from "~/context/auth.withActor" +import { IconCreditCard } from "~/component/icon" + +const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { + "use server" + return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) +}, "checkoutUrl") + +const reload = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key }) +}, "billing.reload") + +const disableReload = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key }) +}, "billing.disableReload") + +const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { + "use server" + return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID) +}, "sessionUrl") + +const getBillingInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.get() + }, workspaceID) +}, "billing.get") + +export function BillingSection() { + const params = useParams() + const balanceInfo = createAsync(() => getBillingInfo(params.id)) + const createCheckoutUrlAction = useAction(createCheckoutUrl) + const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) + const createSessionUrlAction = useAction(createSessionUrl) + const createSessionUrlSubmission = useSubmission(createSessionUrl) + const disableReloadSubmission = useSubmission(disableReload) + const reloadSubmission = useSubmission(reload) + + const balanceAmount = createMemo(() => { + return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) + }) + + return ( +
+
+

Billing

+

+ Manage payments methods. Contact us if you have any questions. +

+
+
+ +
+

+ Reload failed at{" "} + {balanceInfo()?.timeReloadError!.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + })} + . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try + again. +

+
+ + +
+
+
+
+
+
+ +
+
+ ----}> + •••• + {balanceInfo()?.paymentMethodLast4} + +
+
+
+ { + const baseUrl = window.location.href + const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) + if (checkoutUrl) { + window.location.href = checkoutUrl + } + }} + > + {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"} + + } + > + +
+ + +
+
+
+
+
+ +

+ You have ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} remaining in + your account. You can continue using the API with your remaining balance. +

+
+ +

+ Your current balance is ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} + . We'll automatically reload $20 (+$1.23 processing fee) when it reaches $5. +

+
+
+
+
+ ) +} diff --git a/cloud/app/src/component/workspace/common.tsx b/cloud/app/src/component/workspace/common.tsx new file mode 100644 index 00000000..fd1b8b1b --- /dev/null +++ b/cloud/app/src/component/workspace/common.tsx @@ -0,0 +1,27 @@ +export function formatDateForTable(date: Date) { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "short", + hour: "numeric", + minute: "2-digit", + hour12: true, + } + return date.toLocaleDateString("en-GB", options).replace(",", ",") +} + +export function formatDateUTC(date: Date) { + const options: Intl.DateTimeFormatOptions = { + weekday: "short", + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + timeZone: "UTC", + } + return date.toLocaleDateString("en-US", options) +} + + diff --git a/cloud/app/src/component/workspace/key-section.tsx b/cloud/app/src/component/workspace/key-section.tsx new file mode 100644 index 00000000..ef160156 --- /dev/null +++ b/cloud/app/src/component/workspace/key-section.tsx @@ -0,0 +1,181 @@ +import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" +import { createEffect, createSignal, For, Show } from "solid-js" +import { IconCopy, IconCheck } from "~/component/icon" +import { Key } from "@opencode/cloud-core/key.js" +import { withActor } from "~/context/auth.withActor" +import { createStore } from "solid-js/store" +import { formatDateUTC, formatDateForTable } from "./common" + +const removeKey = action(async (form: FormData) => { + "use server" + const id = form.get("id")?.toString() + if (!id) return { error: "ID is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key }) +}, "key.remove") + +const createKey = action(async (form: FormData) => { + "use server" + const name = form.get("name")?.toString().trim() + if (!name) return { error: "Name is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json( + await withActor( + () => + Key.create({ name }) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ error: e.message as string })), + workspaceID, + ), + { revalidate: listKeys.key }, + ) +}, "key.create") + +const listKeys = query(async (workspaceID: string) => { + "use server" + return withActor(() => Key.list(), workspaceID) +}, "key.list") + +export function KeyCreateForm() { + const params = useParams() + const submission = useSubmission(createKey) + const [store, setStore] = createStore({ show: false }) + + let input: HTMLInputElement + + createEffect(() => { + if (!submission.pending && submission.result && !submission.result.error) { + hide() + } + }) + + function show() { + // submission.clear() does not clear the result in some cases, ie. + // 1. Create key with empty name => error shows + // 2. Put in a key name and creates the key => form hides + // 3. Click add key button again => form shows with the same error if + // submission.clear() is called only once + while (true) { + submission.clear() + if (!submission.result) break + } + setStore("show", true) + input.focus() + } + + function hide() { + setStore("show", false) + } + + return ( + show()}> + Create API Key + + } + > +
+
+ (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" /> + + {(err) =>
{err()}
} +
+
+ +
+ + +
+
+
+ ) +} + +export function KeySection() { + const params = useParams() + const keys = createAsync(() => listKeys(params.id)) + + function formatKey(key: string) { + if (key.length <= 11) return key + return `${key.slice(0, 7)}...${key.slice(-4)}` + } + + return ( +
+
+

API Keys

+

Manage your API keys for accessing opencode services.

+
+ +
+ +

Create an opencode Gateway API key

+
+ } + > + + + + + + + + + + + + {(key) => { + const [copied, setCopied] = createSignal(false) + // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id) + return ( + + + + + + + ) + }} + + +
NameKeyCreated
{key.name} + + + {formatDateForTable(key.timeCreated)} + +
+ + + +
+
+ + +
+ ) +} diff --git a/cloud/app/src/component/workspace/monthly-limit-section.tsx b/cloud/app/src/component/workspace/monthly-limit-section.tsx new file mode 100644 index 00000000..0ed45478 --- /dev/null +++ b/cloud/app/src/component/workspace/monthly-limit-section.tsx @@ -0,0 +1,129 @@ +import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" +import { createEffect, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { withActor } from "~/context/auth.withActor" +import { Billing } from "@opencode/cloud-core/billing.js" + +const getBillingInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.get() + }, workspaceID) +}, "billing.get") + +const setMonthlyLimit = action(async (form: FormData) => { + "use server" + const limit = form.get("limit")?.toString() + if (!limit) return { error: "Limit is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json( + await withActor( + () => + Billing.setMonthlyLimit(parseInt(limit)) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ error: e.message as string })), + workspaceID, + ), + { revalidate: getBillingInfo.key }, + ) +}, "billing.setMonthlyLimit") + +export function MonthlyLimitSection() { + const params = useParams() + const submission = useSubmission(setMonthlyLimit) + const [store, setStore] = createStore({ show: false }) + const balanceInfo = createAsync(() => getBillingInfo(params.id)) + + let input: HTMLInputElement + + createEffect(() => { + if (!submission.pending && submission.result && !submission.result.error) { + hide() + } + }) + + function show() { + // submission.clear() does not clear the result in some cases, ie. + // 1. Create key with empty name => error shows + // 2. Put in a key name and creates the key => form hides + // 3. Click add key button again => form shows with the same error if + // submission.clear() is called only once + while (true) { + submission.clear() + if (!submission.result) break + } + setStore("show", true) + input.focus() + } + + function hide() { + setStore("show", false) + } + + return ( +
+
+

Monthly Limit

+

Set a monthly spending limit for your account.

+
+
+
+
+ {balanceInfo()?.monthlyLimit ? $ : null} + {balanceInfo()?.monthlyLimit ?? "-"} +
+ +
+ (input = r)} data-component="input" name="limit" type="number" placeholder="50" /> + + {(err) =>
{err()}
} +
+
+ +
+ + +
+ + } + > + +
+
+ No spending limit set.

}> +

+ Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $ + {(() => { + const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated + if (!dateLastUsed) return "0" + + const current = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", + }) + const lastUsed = dateLastUsed.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", + }) + if (current !== lastUsed) return "0" + return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2) + })()} + . +

+
+
+
+ ) +} diff --git a/cloud/app/src/component/workspace/new-user-section.tsx b/cloud/app/src/component/workspace/new-user-section.tsx new file mode 100644 index 00000000..ca38390b --- /dev/null +++ b/cloud/app/src/component/workspace/new-user-section.tsx @@ -0,0 +1,97 @@ +import { query, useParams, createAsync } from "@solidjs/router" +import { createMemo, createSignal, Show } from "solid-js" +import { IconCopy, IconCheck } from "~/component/icon" +import { Key } from "@opencode/cloud-core/key.js" +import { Billing } from "@opencode/cloud-core/billing.js" +import { withActor } from "~/context/auth.withActor" + +const getUsageInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.usages() + }, workspaceID) +}, "usage.list") + +const listKeys = query(async (workspaceID: string) => { + "use server" + return withActor(() => Key.list(), workspaceID) +}, "key.list") + +export function NewUserSection() { + const params = useParams() + const [copiedKey, setCopiedKey] = createSignal(false) + const keys = createAsync(() => listKeys(params.id)) + const usage = createAsync(() => getUsageInfo(params.id)) + const isNew = createMemo(() => { + const keysList = keys() + const usageList = usage() + return keysList?.length === 1 && (!usageList || usageList.length === 0) + }) + const defaultKey = createMemo(() => keys()?.at(-1)?.key) + + return ( + +
+
+
+

Tested & Verified Models

+

We've benchmarked and tested models specifically for coding agents to ensure the best performance.

+
+
+

Highest Quality

+

Access models configured for optimal performance - no downgrades or routing to cheaper providers.

+
+
+

No Lock-in

+

Use Zen with any coding agent, and continue using other providers with opencode whenever you want.

+
+
+ +
+ +
+
+ {defaultKey()} + +
+
+
+
+ +
+
    +
  1. Enable billing
  2. +
  3. + Run opencode auth login and select opencode +
  4. +
  5. Paste your API key
  6. +
  7. + Start opencode and run /models to select a model +
  8. +
+
+
+
+ ) +} + diff --git a/cloud/app/src/component/workspace/payment-section.tsx b/cloud/app/src/component/workspace/payment-section.tsx new file mode 100644 index 00000000..f802fa96 --- /dev/null +++ b/cloud/app/src/component/workspace/payment-section.tsx @@ -0,0 +1,56 @@ +import { Billing } from "@opencode/cloud-core/billing.js" +import { query, useParams, createAsync } from "@solidjs/router" +import { For } from "solid-js" +import { withActor } from "~/context/auth.withActor" +import { formatDateUTC, formatDateForTable } from "./common" + +const getPaymentsInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.payments() + }, workspaceID) +}, "payment.list") + +export function PaymentSection() { + const params = useParams() + const payments = createAsync(() => getPaymentsInfo(params.id)) + + return ( + payments() && + payments()!.length > 0 && ( +
+
+

Payments History

+

Recent payment transactions.

+
+
+ + + + + + + + + + + {(payment) => { + const date = new Date(payment.timeCreated) + return ( + + + + + + ) + }} + + +
DatePayment IDAmount
+ {formatDateForTable(date)} + {payment.id}${((payment.amount ?? 0) / 100000000).toFixed(2)}
+
+
+ ) + ) +} diff --git a/cloud/app/src/component/workspace/usage-section.tsx b/cloud/app/src/component/workspace/usage-section.tsx new file mode 100644 index 00000000..a2ad507a --- /dev/null +++ b/cloud/app/src/component/workspace/usage-section.tsx @@ -0,0 +1,66 @@ +import { Billing } from "@opencode/cloud-core/billing.js" +import { query, useParams, createAsync } from "@solidjs/router" +import { createMemo, For, Show } from "solid-js" +import { formatDateUTC, formatDateForTable } from "./common" +import { withActor } from "~/context/auth.withActor" + +const getUsageInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.usages() + }, workspaceID) +}, "usage.list") + +export function UsageSection() { + const params = useParams() + const usage = createAsync(() => getUsageInfo(params.id)) + + return ( +
+
+

Usage History

+

Recent API usage and costs.

+
+
+ 0} + fallback={ +
+

Make your first API call to get started.

+
+ } + > + + + + + + + + + + + + + {(usage) => { + const date = createMemo(() => new Date(usage.timeCreated)) + return ( + + + + + + + + ) + }} + + +
DateModelInputOutputCost
+ {formatDateForTable(date())} + {usage.model}{usage.inputTokens}{usage.outputTokens}${((usage.cost ?? 0) / 100000000).toFixed(4)}
+
+
+
+ ) +} diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index ad107825..ed1434a6 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/cloud/app/src/routes/workspace/[id].tsx @@ -1,77 +1,14 @@ import "./[id].css" import { Billing } from "@opencode/cloud-core/billing.js" -import { Key } from "@opencode/cloud-core/key.js" -import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router" -import { createEffect, createMemo, createSignal, For, Show } from "solid-js" +import { query, useParams, createAsync } from "@solidjs/router" +import { Show } from "solid-js" import { withActor } from "~/context/auth.withActor" -import { IconCopy, IconCheck, IconCreditCard } from "~/component/icon" -import { createStore } from "solid-js/store" - -function formatDateForTable(date: Date) { - const options: Intl.DateTimeFormatOptions = { - day: "numeric", - month: "short", - hour: "numeric", - minute: "2-digit", - hour12: true, - } - return date.toLocaleDateString("en-GB", options).replace(",", ",") -} - -function formatDateUTC(date: Date) { - const options: Intl.DateTimeFormatOptions = { - weekday: "short", - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - timeZone: "UTC", - } - return date.toLocaleDateString("en-US", options) -} - -///////////////////////////////////// -// Keys related queries and actions -///////////////////////////////////// - -const listKeys = query(async (workspaceID: string) => { - "use server" - return withActor(() => Key.list(), workspaceID) -}, "key.list") - -const createKey = action(async (form: FormData) => { - "use server" - const name = form.get("name")?.toString().trim() - if (!name) return { error: "Name is required" } - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json( - await withActor( - () => - Key.create({ name }) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ error: e.message as string })), - workspaceID, - ), - { revalidate: listKeys.key }, - ) -}, "key.create") - -const removeKey = action(async (form: FormData) => { - "use server" - const id = form.get("id")?.toString() - if (!id) return { error: "ID is required" } - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key }) -}, "key.remove") - -///////////////////////////////////// -// Billing related queries and actions -///////////////////////////////////// +import { MonthlyLimitSection } from "~/component/workspace/monthly-limit-section" +import { NewUserSection } from "~/component/workspace/new-user-section" +import { BillingSection } from "~/component/workspace/billing-section" +import { PaymentSection } from "~/component/workspace/payment-section" +import { UsageSection } from "~/component/workspace/usage-section" +import { KeySection } from "~/component/workspace/key-section" const getBillingInfo = query(async (workspaceID: string) => { "use server" @@ -80,596 +17,6 @@ const getBillingInfo = query(async (workspaceID: string) => { }, workspaceID) }, "billing.get") -const getUsageInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.usages() - }, workspaceID) -}, "usage.list") - -const getPaymentsInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.payments() - }, workspaceID) -}, "payment.list") - -const setMonthlyLimit = action(async (form: FormData) => { - "use server" - const limit = form.get("limit")?.toString() - if (!limit) return { error: "Limit is required" } - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json( - await withActor( - () => - Billing.setMonthlyLimit(parseInt(limit)) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ error: e.message as string })), - workspaceID, - ), - { revalidate: getBillingInfo.key }, - ) -}, "billing.setMonthlyLimit") - -const reload = action(async (form: FormData) => { - "use server" - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key }) -}, "billing.reload") - -const disableReload = action(async (form: FormData) => { - "use server" - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key }) -}, "billing.disableReload") - -const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { - "use server" - return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) -}, "checkoutUrl") - -const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { - "use server" - return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID) -}, "sessionUrl") - -function KeySection() { - const params = useParams() - const keys = createAsync(() => listKeys(params.id)) - - function formatKey(key: string) { - if (key.length <= 11) return key - return `${key.slice(0, 7)}...${key.slice(-4)}` - } - - return ( -
-
-

API Keys

-

Manage your API keys for accessing opencode services.

-
- -
- -

Create an opencode Gateway API key

-
- } - > - - - - - - - - - - - - {(key) => { - const [copied, setCopied] = createSignal(false) - // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id) - return ( - - - - - - - ) - }} - - -
NameKeyCreated
{key.name} - - - {formatDateForTable(key.timeCreated)} - -
- - - -
-
- - -
- ) -} - -function KeyCreateForm() { - const params = useParams() - const submission = useSubmission(createKey) - const [store, setStore] = createStore({ show: false }) - - let input: HTMLInputElement - - createEffect(() => { - if (!submission.pending && submission.result && !submission.result.error) { - hide() - } - }) - - function show() { - // submission.clear() does not clear the result in some cases, ie. - // 1. Create key with empty name => error shows - // 2. Put in a key name and creates the key => form hides - // 3. Click add key button again => form shows with the same error if - // submission.clear() is called only once - while (true) { - submission.clear() - if (!submission.result) break - } - setStore("show", true) - input.focus() - } - - function hide() { - setStore("show", false) - } - - return ( - show()}> - Create API Key - - } - > -
-
- (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" /> - - {(err) =>
{err()}
} -
-
- -
- - -
-
-
- ) -} - -function BillingSection() { - const params = useParams() - const balanceInfo = createAsync(() => getBillingInfo(params.id)) - const createCheckoutUrlAction = useAction(createCheckoutUrl) - const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) - const createSessionUrlAction = useAction(createSessionUrl) - const createSessionUrlSubmission = useSubmission(createSessionUrl) - const disableReloadSubmission = useSubmission(disableReload) - const reloadSubmission = useSubmission(reload) - - const balanceAmount = createMemo(() => { - return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) - }) - - return ( -
-
-

Billing

-

- Manage payments methods. Contact us if you have any questions. -

-
-
- -
-

- Reload failed at{" "} - {balanceInfo()?.timeReloadError!.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - })} - . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try - again. -

-
- - -
-
-
-
-
-
- -
-
- ----}> - •••• - {balanceInfo()?.paymentMethodLast4} - -
-
-
- { - const baseUrl = window.location.href - const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) - if (checkoutUrl) { - window.location.href = checkoutUrl - } - }} - > - {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"} - - } - > - -
- - -
-
-
-
-
- -

- You have ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} remaining in - your account. You can continue using the API with your remaining balance. -

-
- -

- Your current balance is ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} - . We'll automatically reload $20 (+$1.23 processing fee) when it reaches $5. -

-
-
-
-
- ) -} - -function MonthlyLimitSection() { - const params = useParams() - const submission = useSubmission(setMonthlyLimit) - const [store, setStore] = createStore({ show: false }) - const balanceInfo = createAsync(() => getBillingInfo(params.id)) - - let input: HTMLInputElement - - createEffect(() => { - if (!submission.pending && submission.result && !submission.result.error) { - hide() - } - }) - - function show() { - // submission.clear() does not clear the result in some cases, ie. - // 1. Create key with empty name => error shows - // 2. Put in a key name and creates the key => form hides - // 3. Click add key button again => form shows with the same error if - // submission.clear() is called only once - while (true) { - submission.clear() - if (!submission.result) break - } - setStore("show", true) - input.focus() - } - - function hide() { - setStore("show", false) - } - - return ( -
-
-

Monthly Limit

-

Set a monthly spending limit for your account.

-
-
-
-
- {balanceInfo()?.monthlyLimit ? $ : null} - {balanceInfo()?.monthlyLimit ?? "-"} -
- -
- (input = r)} data-component="input" name="limit" type="number" placeholder="50" /> - - {(err) =>
{err()}
} -
-
- -
- - -
- - } - > - -
-
- No spending limit set.

}> -

- Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $ - {(() => { - const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated - if (!dateLastUsed) return "0" - - const current = new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "long", - timeZone: "UTC", - }) - const lastUsed = dateLastUsed.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - timeZone: "UTC", - }) - if (current !== lastUsed) return "0" - return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2) - })()} - . -

-
-
-
- ) -} - -function UsageSection() { - const params = useParams() - const usage = createAsync(() => getUsageInfo(params.id)) - - return ( -
-
-

Usage History

-

Recent API usage and costs.

-
-
- 0} - fallback={ -
-

Make your first API call to get started.

-
- } - > - - - - - - - - - - - - - {(usage) => { - const date = createMemo(() => new Date(usage.timeCreated)) - return ( - - - - - - - - ) - }} - - -
DateModelInputOutputCost
- {formatDateForTable(date())} - {usage.model}{usage.inputTokens}{usage.outputTokens}${((usage.cost ?? 0) / 100000000).toFixed(4)}
-
-
-
- ) -} - -function PaymentSection() { - const params = useParams() - const payments = createAsync(() => getPaymentsInfo(params.id)) - - return ( - payments() && - payments()!.length > 0 && ( -
-
-

Payments History

-

Recent payment transactions.

-
-
- - - - - - - - - - - {(payment) => { - const date = new Date(payment.timeCreated) - return ( - - - - - - ) - }} - - -
DatePayment IDAmount
- {formatDateForTable(date)} - {payment.id}${((payment.amount ?? 0) / 100000000).toFixed(2)}
-
-
- ) - ) -} - -function NewUserSection() { - const params = useParams() - const [copiedKey, setCopiedKey] = createSignal(false) - const keys = createAsync(() => listKeys(params.id)) - const usage = createAsync(() => getUsageInfo(params.id)) - const isNew = createMemo(() => { - const keysList = keys() - const usageList = usage() - return keysList?.length === 1 && (!usageList || usageList.length === 0) - }) - const defaultKey = createMemo(() => keys()?.at(-1)?.key) - - return ( - -
-
-
-

Tested & Verified Models

-

We've benchmarked and tested models specifically for coding agents to ensure the best performance.

-
-
-

Highest Quality

-

Access models configured for optimal performance - no downgrades or routing to cheaper providers.

-
-
-

No Lock-in

-

Use Zen with any coding agent, and continue using other providers with opencode whenever you want.

-
-
- -
- -
-
- {defaultKey()} - -
-
-
-
- -
-
    -
  1. Enable billing
  2. -
  3. - Run opencode auth login and select opencode -
  4. -
  5. Paste your API key
  6. -
  7. - Start opencode and run /models to select a model -
  8. -
-
-
-
- ) -} - export default function () { const params = useParams() const balanceInfo = createAsync(() => getBillingInfo(params.id))