diff --git a/src/slices/main/OpenPositions/components/OpenPositionsList.tsx b/src/slices/main/OpenPositions/components/OpenPositionsList.tsx
new file mode 100644
index 00000000..f796c5fc
--- /dev/null
+++ b/src/slices/main/OpenPositions/components/OpenPositionsList.tsx
@@ -0,0 +1,106 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+
+import type { AshbyJob } from '@/utils/ashby'
+
+import FilterButtonGroup from '@/components/FilterButtonGroup'
+import SvgArrowDownAccordion from '@/components/svg/SvgArrowDownAccordion'
+import { Button } from '@/components/ui/Button'
+
+const VIEW_ALL = 'View All'
+
+export type OpenPositionsListProps = {
+ jobs: AshbyJob[]
+}
+
+export default function OpenPositionsList({ jobs }: OpenPositionsListProps) {
+ const [selectedCategory, setSelectedCategory] = useState
(VIEW_ALL)
+
+ const categories = useMemo(
+ () => [
+ VIEW_ALL,
+ ...new Set(jobs.map(({ category }) => category).filter(Boolean)),
+ ],
+ [jobs]
+ )
+
+ const onCategoryButtonClick = (category: string) => () =>
+ setSelectedCategory(category)
+
+ const filteredJobs = useMemo(
+ () =>
+ selectedCategory === VIEW_ALL
+ ? jobs
+ : jobs.filter(({ category }) => category === selectedCategory),
+ [jobs, selectedCategory]
+ )
+
+ if (jobs.length === 0) {
+ return (
+
+ There are no open positions right now — check back soon.
+
+ )
+ }
+
+ return (
+ <>
+
+ buttons={categories}
+ onButtonClick={onCategoryButtonClick}
+ selectedButtons={selectedCategory}
+ />
+
+ >
+ )
+}
diff --git a/src/utils/ashby.ts b/src/utils/ashby.ts
new file mode 100644
index 00000000..ed8ca4c5
--- /dev/null
+++ b/src/utils/ashby.ts
@@ -0,0 +1,93 @@
+/**
+ * Server-side fetch of open roles from Ashby's public Job Posting API.
+ *
+ * Docs: https://developers.ashbyhq.com/reference/job-posting-api
+ * The board name is the slug in your Ashby board URL (jobs.ashbyhq.com/).
+ */
+
+const ASHBY_JOB_BOARD_NAME =
+ process.env.NEXT_PUBLIC_ASHBY_JOB_BOARD_NAME?.trim() || 'Plural'
+
+const ASHBY_POSTING_API = `https://api.ashbyhq.com/posting-api/job-board/${ASHBY_JOB_BOARD_NAME}?includeCompensation=true`
+
+// Roles change often; re-fetch at most once an hour (ISR).
+const REVALIDATE_SECONDS = 60 * 60
+
+/** Raw shape returned by the Ashby posting API (only the fields we use). */
+type AshbyApiJob = {
+ id: string
+ title: string
+ team?: string
+ department?: string
+ location?: string
+ workplaceType?: string
+ employmentType?: string
+ isRemote?: boolean
+ isListed?: boolean
+ jobUrl: string
+ applyUrl: string
+ compensation?: {
+ compensationTierSummary?: string | null
+ } | null
+}
+
+type AshbyApiResponse = {
+ jobs?: AshbyApiJob[]
+}
+
+/** Normalized job used by the UI. */
+export type AshbyJob = {
+ id: string
+ title: string
+ /** Team/department — used as the filterable category. */
+ category: string
+ /** Human-readable location, e.g. "New York City · Hybrid". */
+ location: string
+ /** Compensation summary, e.g. "$175K – $200K • Offers Equity", if published. */
+ compensation: string | null
+ /** External Ashby posting URL (description + apply button). */
+ url: string
+}
+
+function toLocationLabel(job: AshbyApiJob): string {
+ const parts = [job.location, job.workplaceType].filter(Boolean)
+ return parts.join(' · ')
+}
+
+function normalize(job: AshbyApiJob): AshbyJob {
+ return {
+ id: job.id,
+ title: job.title,
+ category: job.team || job.department || '',
+ location: toLocationLabel(job),
+ compensation: job.compensation?.compensationTierSummary ?? null,
+ url: job.jobUrl,
+ }
+}
+
+/**
+ * Fetches the currently-listed open roles from Ashby. Runs on the server,
+ * so it avoids CORS and keeps the third-party script off the page. Returns an
+ * empty array on any error so the section degrades gracefully.
+ */
+export async function getAshbyJobs(): Promise {
+ try {
+ const res = await fetch(ASHBY_POSTING_API, {
+ next: { revalidate: REVALIDATE_SECONDS },
+ })
+
+ if (!res.ok) {
+ console.error(`Ashby posting API returned ${res.status}`)
+ return []
+ }
+
+ const data = (await res.json()) as AshbyApiResponse
+
+ return (data.jobs ?? [])
+ .filter((job) => job.isListed !== false)
+ .map(normalize)
+ } catch (error) {
+ console.error('Failed to fetch Ashby jobs', error)
+ return []
+ }
+}