Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/en/resources/glossary/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,16 @@ Standard tools are the default tier and cover the majority of Arcade's catalog.

Pro tools incur materially higher operational cost than Standard tools due to underlying infrastructure (e.g., compute-intensive sandboxes), provider-imposed fees (e.g., per-call API charges from data providers), or tool complexity. Each invocation of a Pro tool counts as one Pro Tool Execution against your plan's monthly Pro allowance, with any overage billed per execution at the Pro rate listed on the [pricing page](https://www.arcade.dev/pricing). Arcade may reclassify tools between Standard and Pro from time to time as the underlying cost structure of a tool changes; any such reclassification applies prospectively.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google.Latin: Replace 'e.g.' with 'for example' in both instances

Suggested change
Pro tools incur materially higher operational cost than Standard tools due to underlying infrastructure (e.g., compute-intensive sandboxes), provider-imposed fees (e.g., per-call API charges from data providers), or tool complexity. Each invocation of a Pro tool counts as one Pro Tool Execution against your plan's monthly Pro allowance, with any overage billed per execution at the Pro rate listed on the [pricing page](https://www.arcade.dev/pricing). Arcade may reclassify tools between Standard and Pro from time to time as the underlying cost structure of a tool changes; any such reclassification applies prospectively.
Pro tools incur materially higher operational cost than Standard tools due to underlying infrastructure (for example, compute-intensive sandboxes), provider-imposed fees (for example, per-call API charges from data providers), or tool complexity. Each invocation of a Pro tool counts as one Pro Tool Execution against your plan's monthly Pro allowance, with any overage billed per execution at the Pro rate listed on the [pricing page](https://www.arcade.dev/pricing). Arcade may reclassify tools between Standard and Pro from time to time as the underlying cost structure of a tool changes; any such reclassification applies prospectively.


As of 04/24, Pro tools include: E2B, Firecrawl, Google Finance, Google Flights, Google Hotels, Google Jobs, Google Maps, Google News, Google Search, Google Shopping, Imgflip, Walmart, and YouTube.
See the current list of [Pro tools](/en/resources/integrations?pro=1).

### Bring Your Own Credentials (BYOC)

Bring Your Own Credentials (BYOC) is a feature that allows you to use your own credentials to access Pro tools. This changes the cost of the tool execution, as you will be charged directly by the provider of the tool, rather than relying on Arcade to pay the bill for you. In exchange, the tool execution will be billed at the Standard rate. As of 04/26/2026, most credentials required for Pro tools are API Keys specific to the service being accessed.
Bring Your Own Credentials (BYOC) is a feature that allows you to use your own credentials to access Pro tools. This changes the cost of the tool execution, as you will be charged directly by the provider of the tool, rather than relying on Arcade to pay the bill for you. In exchange, the tool execution will be billed at the Standard rate. Most credentials required for Pro tools are API Keys specific to the service being accessed.

To set your own credentials, set the requisite secret(s) within the Arcade Dashboard Secrets page, overwriting the default 'static' credentials. You can also set the secrets using the Arcade CLI.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google.OptionalPlurals: Remove (s) from secret(s)

Suggested change
To set your own credentials, set the requisite secret(s) within the Arcade Dashboard Secrets page, overwriting the default 'static' credentials. You can also set the secrets using the Arcade CLI.
To set your own credentials, set the requisite secret within the Arcade Dashboard Secrets page, overwriting the default 'static' credentials. You can also set the secrets using the Arcade CLI.


See the current list of [BYOC-eligible tools](/en/resources/integrations?byoc=1).

## Tool Execution and Tool Development

```mermaid
Expand Down
3 changes: 3 additions & 0 deletions app/en/resources/integrations/components/toolkits-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { FiltersBar } from "./filters-bar";
import { ToolCard } from "./tool-card";
import { TYPE_CONFIG, TYPE_DESCRIPTIONS } from "./type-config";
import { useFilterStore, useToolkitFilters } from "./use-toolkit-filters";
import { useUrlFilterSync } from "./use-url-filter-sync";

type ToolkitsClientProps = {
toolkits: ToolkitWithDocsLink[];
Expand Down Expand Up @@ -63,6 +64,8 @@ function getToolkitIconWithFallback(
}

export default function ToolkitsClient({ toolkits }: ToolkitsClientProps) {
useUrlFilterSync();

const clearAllFilters = useFilterStore((state) => state.clearAllFilters);

const { hasActiveFilters, filteredToolkits, resultsCount } =
Expand Down
148 changes: 148 additions & 0 deletions app/en/resources/integrations/components/use-url-filter-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"use client";

import type { ToolkitType } from "@arcadeai/design-system";
import { useEffect, useRef } from "react";
import { useFilterStore } from "./use-toolkit-filters";

const VALID_TYPES = new Set<string>([
"arcade",
"arcade_starter",
"verified",
"community",
"auth",
]);

const PARAM_CATEGORY = "category";
const PARAM_TYPE = "type";
const PARAM_PRO = "pro";
const PARAM_BYOC = "byoc";
const PARAM_SEARCH = "q";

function isTruthy(value: string | null): boolean {
return value !== null && value !== "" && value !== "0" && value !== "false";
}

export type ParsedFilters = Partial<{
selectedCategory: string;
selectedType: ToolkitType | "all";
filterByPro: boolean;
filterByByoc: boolean;
searchQuery: string;
}>;

export function parseFiltersFromParams(search: string): ParsedFilters {
const params = new URLSearchParams(search);
const result: ParsedFilters = {};

const category = params.get(PARAM_CATEGORY);
if (category && category !== "all") {
result.selectedCategory = category;
}

const type = params.get(PARAM_TYPE);
if (type && VALID_TYPES.has(type)) {
result.selectedType = type as ToolkitType;
}

if (params.has(PARAM_PRO) && isTruthy(params.get(PARAM_PRO))) {
result.filterByPro = true;
}

if (params.has(PARAM_BYOC) && isTruthy(params.get(PARAM_BYOC))) {
result.filterByByoc = true;
}

const q = params.get(PARAM_SEARCH);
if (q) {
result.searchQuery = q;
}

return result;
}

export type SerializableFilterState = {
selectedCategory: string;
selectedType: string;
filterByPro: boolean;
filterByByoc: boolean;
searchQuery: string;
};

export function serializeFiltersToParams(
state: SerializableFilterState
): string {
const params = new URLSearchParams();

if (state.selectedCategory !== "all") {
params.set(PARAM_CATEGORY, state.selectedCategory);
}
if (state.selectedType !== "all") {
params.set(PARAM_TYPE, state.selectedType);
}
if (state.filterByPro) {
params.set(PARAM_PRO, "1");
}
if (state.filterByByoc) {
params.set(PARAM_BYOC, "1");
}
if (state.searchQuery) {
params.set(PARAM_SEARCH, state.searchQuery);
}

return params.toString();
}

function writeFiltersToUrl(state: SerializableFilterState): void {
const qs = serializeFiltersToParams(state);
const newUrl = qs
? `${window.location.pathname}?${qs}`
: window.location.pathname;

if (newUrl !== `${window.location.pathname}${window.location.search}`) {
window.history.replaceState(null, "", newUrl);
}
}

/**
* Two-way sync between the Zustand filter store and URL query params.
*
* - Mount: URL → store (so shared/bookmarked links work)
* - Filter change: store → URL (via replaceState, no navigation)
* - Back/forward: URL → store (via popstate listener)
*/
export function useUrlFilterSync(): void {
const hydrated = useRef(false);

useEffect(() => {
if (!hydrated.current) {
hydrated.current = true;
const fromUrl = parseFiltersFromParams(window.location.search);
if (Object.keys(fromUrl).length > 0) {
useFilterStore.setState(fromUrl);
}
}

const unsubscribe = useFilterStore.subscribe((state) => {
writeFiltersToUrl(state);
});

const handlePopState = () => {
const defaults = {
selectedCategory: "all",
selectedType: "all" as const,
filterByPro: false,
filterByByoc: false,
searchQuery: "",
};
const fromUrl = parseFiltersFromParams(window.location.search);
useFilterStore.setState({ ...defaults, ...fromUrl });
};

window.addEventListener("popstate", handlePopState);

return () => {
unsubscribe();
window.removeEventListener("popstate", handlePopState);
};
}, []);
}
170 changes: 170 additions & 0 deletions tests/url-filter-sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { describe, expect, test } from "vitest";
import {
parseFiltersFromParams,
type SerializableFilterState,
serializeFiltersToParams,
} from "../app/en/resources/integrations/components/use-url-filter-sync";

const DEFAULT_STATE: SerializableFilterState = {
selectedCategory: "all",
selectedType: "all",
filterByPro: false,
filterByByoc: false,
searchQuery: "",
};

describe("parseFiltersFromParams", () => {
test("returns empty object for no params", () => {
expect(parseFiltersFromParams("")).toEqual({});
});

test("parses pro filter", () => {
expect(parseFiltersFromParams("?pro=1")).toEqual({ filterByPro: true });
});

test("parses byoc filter", () => {
expect(parseFiltersFromParams("?byoc=1")).toEqual({ filterByByoc: true });
});

test("parses pro + byoc together", () => {
expect(parseFiltersFromParams("?pro=1&byoc=1")).toEqual({
filterByPro: true,
filterByByoc: true,
});
});

test("parses category", () => {
expect(parseFiltersFromParams("?category=development")).toEqual({
selectedCategory: "development",
});
});

test("parses type", () => {
expect(parseFiltersFromParams("?type=arcade")).toEqual({
selectedType: "arcade",
});
});

test("parses search query", () => {
expect(parseFiltersFromParams("?q=gmail")).toEqual({
searchQuery: "gmail",
});
});

test("parses all params at once", () => {
expect(
parseFiltersFromParams(
"?category=productivity&type=verified&pro=1&byoc=1&q=slack"
)
).toEqual({
selectedCategory: "productivity",
selectedType: "verified",
filterByPro: true,
filterByByoc: true,
searchQuery: "slack",
});
});

test("ignores invalid type values", () => {
expect(parseFiltersFromParams("?type=bogus")).toEqual({});
});

test("ignores falsy pro values", () => {
expect(parseFiltersFromParams("?pro=0")).toEqual({});
expect(parseFiltersFromParams("?pro=false")).toEqual({});
expect(parseFiltersFromParams("?pro=")).toEqual({});
});

test("ignores category=all", () => {
expect(parseFiltersFromParams("?category=all")).toEqual({});
});

test("ignores unknown params gracefully", () => {
expect(parseFiltersFromParams("?foo=bar&baz=1")).toEqual({});
});
});

describe("serializeFiltersToParams", () => {
test("returns empty string for default state", () => {
expect(serializeFiltersToParams(DEFAULT_STATE)).toBe("");
});

test("serializes pro filter", () => {
expect(
serializeFiltersToParams({ ...DEFAULT_STATE, filterByPro: true })
).toBe("pro=1");
});

test("serializes byoc filter", () => {
expect(
serializeFiltersToParams({ ...DEFAULT_STATE, filterByByoc: true })
).toBe("byoc=1");
});

test("serializes category", () => {
expect(
serializeFiltersToParams({
...DEFAULT_STATE,
selectedCategory: "development",
})
).toBe("category=development");
});

test("serializes type", () => {
expect(
serializeFiltersToParams({
...DEFAULT_STATE,
selectedType: "arcade",
})
).toBe("type=arcade");
});

test("serializes search query", () => {
expect(
serializeFiltersToParams({ ...DEFAULT_STATE, searchQuery: "gmail" })
).toBe("q=gmail");
});

test("serializes all filters at once", () => {
const qs = serializeFiltersToParams({
selectedCategory: "productivity",
selectedType: "verified",
filterByPro: true,
filterByByoc: true,
searchQuery: "slack",
});
const params = new URLSearchParams(qs);
expect(params.get("category")).toBe("productivity");
expect(params.get("type")).toBe("verified");
expect(params.get("pro")).toBe("1");
expect(params.get("byoc")).toBe("1");
expect(params.get("q")).toBe("slack");
});
});

describe("round-trip", () => {
test("serialize then parse produces equivalent filters", () => {
const state: SerializableFilterState = {
selectedCategory: "social",
selectedType: "community",
filterByPro: true,
filterByByoc: false,
searchQuery: "twitter",
};
const qs = serializeFiltersToParams(state);
const parsed = parseFiltersFromParams(`?${qs}`);
expect(parsed).toEqual({
selectedCategory: "social",
selectedType: "community",
filterByPro: true,
searchQuery: "twitter",
});
});

test("default state round-trips to empty", () => {
const qs = serializeFiltersToParams(DEFAULT_STATE);
expect(qs).toBe("");
const parsed = parseFiltersFromParams(qs);
expect(parsed).toEqual({});
});
});
Loading