diff --git a/apps/web/package.json b/apps/web/package.json
index 1dea5e859..46c04e555 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -42,6 +42,7 @@
"flatpickr": "^4.6.13",
"formik": "^2.4.6",
"framer-motion": "^12.12.1",
+ "graphql-request": "^7.1.2",
"ioredis": "^5.8.1",
"next": "^15.5.9",
"nextjs-progressbar": "^0.0.16",
@@ -49,6 +50,7 @@
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-mde": "^11.5.0",
+ "remove-markdown": "^0.6.3",
"sablier": "^2.0.1",
"sharp": "^0.34.2",
"swr": "^2.3.3",
diff --git a/apps/web/public/builderlogo.png b/apps/web/public/builderlogo.png
new file mode 100644
index 000000000..1b06d6e5c
Binary files /dev/null and b/apps/web/public/builderlogo.png differ
diff --git a/apps/web/public/noggles-square-dark.svg b/apps/web/public/noggles-square-dark.svg
new file mode 100644
index 000000000..2eae203f8
--- /dev/null
+++ b/apps/web/public/noggles-square-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/apps/web/public/noggles-square.svg b/apps/web/public/noggles-square.svg
new file mode 100644
index 000000000..c4979bf04
--- /dev/null
+++ b/apps/web/public/noggles-square.svg
@@ -0,0 +1,7 @@
+
diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts
new file mode 100644
index 000000000..3c9fec2c0
--- /dev/null
+++ b/apps/web/src/modules/about/AboutPage.css.ts
@@ -0,0 +1,1593 @@
+import { theme } from '@buildeross/zord'
+import { globalStyle, keyframes, style } from '@vanilla-extract/css'
+
+const focusRing = {
+ outline: `2px solid ${theme.colors.focusRing}`,
+ outlineOffset: '2px',
+}
+
+const standardBorder = `2px solid ${theme.colors.border}`
+const standardBorderThin = `1px solid ${theme.colors.border}`
+const softBlueBackground = `color-mix(in srgb, ${theme.colors.focusRing} 14%, ${theme.colors.background1})`
+const softBlueBorder = theme.colors.focusRing
+
+const marqueeScroll = keyframes({
+ '0%': {
+ transform: 'translateX(0)',
+ },
+ '100%': {
+ transform: 'translateX(-50%)',
+ },
+})
+
+export const page = style({
+ width: '100%',
+ padding: '48px 16px 96px',
+ boxSizing: 'border-box',
+ background: theme.colors.background1,
+ overflowX: 'clip',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ padding: '64px 24px 120px',
+ },
+ },
+})
+
+export const container = style({
+ maxWidth: '1180px',
+ margin: '0 auto',
+ width: '100%',
+ minWidth: 0,
+})
+
+export const centeredImageWrap = style({
+ display: 'flex',
+ justifyContent: 'center',
+})
+
+export const centeredImage = style({
+ width: '280px',
+ maxWidth: '100%',
+ height: 'auto',
+ display: 'block',
+ selectors: {
+ 'html[data-theme-mode="dark"] &': {
+ filter: 'invert(1)',
+ },
+ },
+})
+
+export const heroVennWrap = style({
+ width: '100%',
+ height: '100%',
+ minHeight: '220px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '12px',
+})
+
+export const heroVennImage = style({
+ width: '100%',
+ maxWidth: '260px',
+ height: 'auto',
+ display: 'block',
+})
+
+const darkModeSelector = 'html[data-theme-mode="dark"] &'
+
+export const aboutLightOnly = style({
+ display: 'block !important',
+ selectors: {
+ [darkModeSelector]: {
+ display: 'none !important',
+ },
+ },
+})
+
+export const aboutDarkOnly = style({
+ display: 'none !important',
+ selectors: {
+ [darkModeSelector]: {
+ display: 'block !important',
+ },
+ },
+})
+
+export const section = style({
+ marginTop: '72px',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ marginTop: '96px',
+ },
+ },
+})
+
+export const sectionHeader = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '12px',
+ marginBottom: '28px',
+ maxWidth: '720px',
+})
+
+export const eyebrow = style({
+ display: 'inline-flex',
+ width: 'fit-content',
+ borderRadius: '999px',
+ padding: '4px 10px',
+ background: theme.colors.background2,
+ border: standardBorderThin,
+ fontSize: '12px',
+ lineHeight: '16px',
+ letterSpacing: '0.04em',
+ textTransform: 'uppercase',
+ color: theme.colors.text3,
+ fontWeight: 700,
+})
+
+export const sectionTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '28px',
+ lineHeight: 1.04,
+ whiteSpace: 'normal',
+ overflowWrap: 'anywhere',
+ color: theme.colors.text1,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '40px',
+ whiteSpace: 'nowrap',
+ overflowWrap: 'normal',
+ },
+ },
+})
+
+export const sectionTitleOnly = style({
+ marginBottom: '12px',
+})
+
+export const sectionCopy = style({
+ fontSize: '16px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+})
+
+export const introCopyNoWrap = style({
+ fontSize: '16px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+ whiteSpace: 'normal',
+ '@media': {
+ 'screen and (min-width: 960px)': {
+ whiteSpace: 'nowrap',
+ },
+ },
+})
+
+export const hero = style({
+ display: 'grid',
+ gap: '28px',
+ alignItems: 'stretch',
+ '@media': {
+ 'screen and (min-width: 980px)': {
+ gridTemplateColumns: 'minmax(0, 1.1fr) minmax(360px, 0.9fr)',
+ gap: '40px',
+ },
+ },
+})
+
+export const heroCopy = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '20px',
+ paddingTop: '8px',
+ minWidth: 0,
+})
+
+export const heroTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '36px',
+ lineHeight: 1,
+ letterSpacing: '-0.03em',
+ color: theme.colors.text1,
+ maxWidth: '760px',
+ overflowWrap: 'anywhere',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '52px',
+ },
+ },
+})
+
+export const heroText = style({
+ maxWidth: '640px',
+ fontSize: '16px',
+ lineHeight: 1.6,
+ color: theme.colors.text3,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '18px',
+ lineHeight: 1.65,
+ },
+ },
+})
+
+export const heroHighlightList = style({
+ display: 'grid',
+ gap: '10px',
+ margin: 0,
+ padding: 0,
+ listStyle: 'none',
+ marginTop: '-4px',
+})
+
+export const heroHighlight = style({
+ display: 'flex',
+ alignItems: 'flex-start',
+ gap: '10px',
+ fontSize: '14px',
+ lineHeight: 1.5,
+ color: theme.colors.text2,
+})
+
+export const heroHighlightDot = style({
+ width: '8px',
+ height: '8px',
+ borderRadius: '999px',
+ background: theme.colors.text1,
+ flexShrink: 0,
+ marginTop: '6px',
+})
+
+export const heroActions = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '12px',
+ alignItems: 'stretch',
+ '@media': {
+ 'screen and (min-width: 640px)': {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ },
+})
+
+export const heroPanel = style({
+ position: 'relative',
+ overflow: 'hidden',
+ borderRadius: '16px',
+ border: standardBorder,
+ background: theme.colors.background2,
+ padding: '14px',
+ minHeight: 'auto',
+ boxShadow: 'none',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ padding: '16px',
+ minHeight: '420px',
+ },
+ },
+})
+
+export const heroPanelGlow = style({
+ position: 'absolute',
+ inset: 0,
+ pointerEvents: 'none',
+ background: 'transparent',
+})
+
+export const montageGrid = style({
+ position: 'relative',
+ zIndex: 1,
+ display: 'grid',
+ gap: '12px',
+ gridTemplateColumns: '1fr',
+ gridTemplateAreas: '"primary" "side" "footer"',
+ '@media': {
+ 'screen and (min-width: 541px)': {
+ gridTemplateColumns: '1.05fr 0.95fr',
+ gridTemplateAreas: '"primary side" "footer footer"',
+ },
+ },
+})
+
+export const montageCard = style({
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '16px',
+ boxShadow: 'none',
+ minWidth: 0,
+})
+
+export const montagePrimary = style({
+ gridArea: 'primary',
+ minHeight: 'unset',
+})
+
+export const montageSide = style({
+ gridArea: 'side',
+ minHeight: '180px',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '220px',
+ },
+ },
+})
+
+export const montageSecondary = style({
+ gridArea: 'secondary',
+ minHeight: '150px',
+})
+
+export const montageFooter = style({
+ gridArea: 'footer',
+ minHeight: '120px',
+ padding: '10px',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '180px',
+ },
+ },
+})
+
+export const logoMarquee = style({
+ width: '100%',
+ height: '100%',
+ minHeight: '96px',
+ overflow: 'hidden',
+ borderRadius: '10px',
+ background: theme.colors.background1,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '160px',
+ },
+ },
+})
+
+export const logoMarqueeTrack = style({
+ position: 'relative',
+ overflow: 'hidden',
+ width: '100%',
+ height: '100%',
+ selectors: {
+ '&::before': {
+ content: '',
+ position: 'absolute',
+ inset: 0,
+ background:
+ 'linear-gradient(90deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 10%, rgba(255, 255, 255, 0) 90%, rgba(255, 255, 255, 1) 100%)',
+ zIndex: 2,
+ pointerEvents: 'none',
+ },
+ },
+})
+
+export const logoMarqueeInner = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '12px',
+ width: 'max-content',
+ minWidth: '100%',
+ height: '100%',
+ padding: '14px 0',
+ animation: `${marqueeScroll} 28s linear infinite`,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ gap: '18px',
+ padding: '22px 0',
+ },
+ },
+})
+
+export const logoMarqueeItem = style({
+ width: '64px',
+ height: '64px',
+ borderRadius: '12px',
+ overflow: 'hidden',
+ background: theme.colors.background1,
+ boxShadow: 'none',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ width: '86px',
+ height: '86px',
+ borderRadius: '22px',
+ },
+ },
+})
+
+export const logoMarqueeImage = style({
+ width: '100%',
+ height: '100%',
+ objectFit: 'cover',
+ display: 'block',
+})
+
+export const montageLabel = style({
+ fontSize: '12px',
+ letterSpacing: '0.08em',
+ textTransform: 'uppercase',
+ color: theme.colors.text4,
+ fontWeight: 700,
+})
+
+export const montageValue = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '24px',
+ lineHeight: 1,
+ whiteSpace: 'pre-line',
+ color: theme.colors.text1,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ marginTop: '10px',
+ fontSize: '28px',
+ },
+ },
+})
+
+export const montageBody = style({
+ marginTop: '10px',
+ fontSize: '14px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+})
+
+export const daoMiniList = style({
+ display: 'grid',
+ gap: '10px',
+ marginTop: '14px',
+})
+
+export const daoMiniCard = style({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: '12px',
+ padding: '12px 14px',
+ borderRadius: '16px',
+ background: theme.colors.background2,
+})
+
+export const daoMiniAvatar = style({
+ width: '40px',
+ height: '40px',
+ borderRadius: '14px',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontWeight: 700,
+ fontSize: '14px',
+ color: theme.colors.text1,
+})
+
+export const heroFooterStat = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '6px',
+})
+
+export const heroFooterValue = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '24px',
+ color: theme.colors.text1,
+})
+
+export const statGrid = style({
+ display: 'grid',
+ width: '100%',
+ gap: '16px',
+ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',
+ '@media': {
+ 'screen and (min-width: 640px)': {
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ },
+ 'screen and (min-width: 1080px)': {
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
+ },
+ },
+})
+
+export const statCard = style({
+ position: 'relative',
+ overflow: 'hidden',
+ minHeight: '178px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '22px',
+ boxShadow: 'none',
+ transition: 'border-color 0.15s ease, background-color 0.15s ease',
+ selectors: {
+ '&:hover': {
+ borderColor: softBlueBorder,
+ backgroundColor: softBlueBackground,
+ },
+ },
+})
+
+export const statAccent = style({
+ position: 'absolute',
+ top: '18px',
+ right: '18px',
+ width: '54px',
+ height: '54px',
+ borderRadius: '12px',
+ opacity: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontSize: '28px',
+ lineHeight: 1,
+})
+
+export const statAccentDao = style({ background: theme.colors.background2 })
+export const statAccentTreasury = style({ background: theme.colors.positive })
+export const statAccentAuction = style({ background: theme.colors.warning })
+export const statAccentProposal = style({ background: theme.colors.focusRing })
+export const statAccentMembers = style({ background: theme.colors.negative })
+export const statAccentTokens = style({ background: theme.colors.focusRing })
+
+export const statLabel = style({
+ position: 'relative',
+ zIndex: 1,
+ fontSize: '13px',
+ fontWeight: 700,
+ letterSpacing: '0.06em',
+ textTransform: 'uppercase',
+ color: theme.colors.text4,
+})
+
+export const statValue = style({
+ position: 'relative',
+ zIndex: 1,
+ marginTop: '18px',
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '36px',
+ lineHeight: 1,
+ color: theme.colors.text1,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '42px',
+ },
+ },
+})
+
+export const statDetail = style({
+ position: 'relative',
+ zIndex: 1,
+ marginTop: '18px',
+ maxWidth: '28ch',
+ fontSize: '14px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+})
+
+export const sectionTopRow = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '18px',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ marginBottom: '28px',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ },
+ },
+})
+
+export const sectionInlineRow = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '18px',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ marginBottom: '28px',
+ '@media': {
+ 'screen and (min-width: 960px)': {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ },
+})
+
+export const sectionInlineCopy = style({
+ maxWidth: '760px',
+ fontSize: '16px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+ margin: 0,
+})
+
+export const tabs = style({
+ display: 'flex',
+ flexWrap: 'wrap',
+ gap: '10px',
+ '@media': {
+ 'screen and (max-width: 520px)': {
+ width: '100%',
+ flexWrap: 'nowrap',
+ gap: '6px',
+ },
+ },
+})
+
+export const tabButton = style({
+ minHeight: '42px',
+ borderRadius: '999px',
+ border: standardBorderThin,
+ background: theme.colors.background1,
+ padding: '10px 16px',
+ fontSize: '14px',
+ fontWeight: 700,
+ color: theme.colors.text1,
+ transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease',
+ selectors: {
+ '&:hover': {
+ cursor: 'pointer',
+ borderColor: softBlueBorder,
+ backgroundColor: softBlueBackground,
+ },
+ '&:focus-visible': focusRing,
+ },
+ '@media': {
+ 'screen and (max-width: 520px)': {
+ flex: '1 1 0',
+ minWidth: 0,
+ minHeight: '38px',
+ padding: '8px 6px',
+ fontSize: '13px',
+ },
+ },
+})
+
+export const activeTabButton = style({
+ background: softBlueBackground,
+ borderColor: softBlueBorder,
+ color: theme.colors.text1,
+ selectors: {
+ '&:hover': {
+ borderColor: softBlueBorder,
+ backgroundColor: softBlueBackground,
+ color: theme.colors.text1,
+ },
+ },
+})
+
+export const daoGrid = style({
+ display: 'grid',
+ width: '100%',
+ gap: '16px',
+ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',
+ '@media': {
+ 'screen and (min-width: 700px)': {
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ },
+ 'screen and (min-width: 1080px)': {
+ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
+ },
+ },
+})
+
+export const mobileStatsHeading = style({
+ display: 'block',
+ margin: '28px 0 14px',
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '24px',
+ lineHeight: 1.1,
+ color: theme.colors.text1,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ display: 'none',
+ },
+ },
+})
+
+export const statsBlock = style({
+ marginTop: '28px',
+ '@media': {
+ 'screen and (max-width: 767px)': {
+ marginTop: 0,
+ },
+ 'screen and (min-width: 768px)': {
+ marginTop: '48px',
+ },
+ },
+})
+
+export const daoCard = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '18px',
+ minHeight: '320px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '18px',
+ boxShadow: 'none',
+ minWidth: 0,
+ color: 'inherit',
+ textDecoration: 'none',
+ transition: 'border-color 0.15s ease, background-color 0.15s ease',
+ selectors: {
+ '&:hover': {
+ borderColor: softBlueBorder,
+ backgroundColor: softBlueBackground,
+ cursor: 'pointer',
+ },
+ '&:focus-visible': focusRing,
+ },
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '360px',
+ },
+ },
+})
+
+export const daoTop = style({
+ display: 'flex',
+ justifyContent: 'space-between',
+ gap: '12px',
+ alignItems: 'flex-start',
+})
+
+export const daoChainBadge = style({
+ width: '34px',
+ height: '34px',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+})
+
+export const daoChainBadgeImage = style({
+ width: '20px',
+ height: '20px',
+ objectFit: 'contain',
+ display: 'block',
+})
+
+export const daoIdentity = style({
+ display: 'flex',
+ gap: '12px',
+ alignItems: 'flex-start',
+ minWidth: 0,
+})
+
+export const daoAvatar = style({
+ width: '52px',
+ height: '52px',
+ borderRadius: '999px',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '18px',
+ fontWeight: 700,
+ flexShrink: 0,
+ overflow: 'hidden',
+})
+
+export const daoAvatarSurfaceA = style({
+ background: theme.colors.background2,
+ color: theme.colors.text1,
+})
+export const daoAvatarSurfaceB = style({
+ background: `color-mix(in srgb, ${theme.colors.focusRing} 22%, ${theme.colors.background1})`,
+ color: theme.colors.text1,
+})
+export const daoAvatarSurfaceC = style({
+ background: theme.colors.positive,
+ color: theme.colors.text1,
+})
+export const daoAvatarSurfaceD = style({
+ background: theme.colors.warning,
+ color: theme.colors.text1,
+})
+
+export const daoAvatarImage = style({
+ width: '100%',
+ height: '100%',
+ borderRadius: 'inherit',
+ objectFit: 'cover',
+ display: 'block',
+})
+
+export const daoName = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '20px',
+ lineHeight: 1.05,
+ color: theme.colors.text1,
+ overflowWrap: 'anywhere',
+})
+
+export const daoDescription = style({
+ marginTop: '6px',
+ fontSize: '14px',
+ lineHeight: 1.6,
+ color: theme.colors.text3,
+})
+
+globalStyle(`${daoDescription} p`, {
+ margin: 0,
+})
+
+globalStyle(`${daoDescription} strong`, {
+ fontWeight: 700,
+ color: theme.colors.text2,
+})
+
+export const badge = style({
+ display: 'inline-flex',
+ width: 'fit-content',
+ borderRadius: '999px',
+ border: standardBorderThin,
+ padding: '6px 10px',
+ fontSize: '12px',
+ fontWeight: 700,
+})
+
+export const daoSignal = style({
+ display: 'grid',
+ gap: '6px',
+ marginTop: 'auto',
+ padding: '14px 16px',
+ borderRadius: '10px',
+ background: theme.colors.background2,
+})
+
+export const daoSignalLabel = style({
+ fontSize: '12px',
+ textTransform: 'uppercase',
+ letterSpacing: '0.06em',
+ color: theme.colors.text4,
+ fontWeight: 700,
+})
+
+export const daoSignalValue = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '18px',
+ lineHeight: 1.2,
+ whiteSpace: 'normal',
+ overflowWrap: 'anywhere',
+ color: theme.colors.text1,
+})
+
+export const cardLink = style({
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: '10px',
+ color: theme.colors.text1,
+ fontSize: '14px',
+ fontWeight: 700,
+ textDecoration: 'none',
+ selectors: {
+ '&:hover': {
+ opacity: 0.7,
+ },
+ '&:focus-visible': focusRing,
+ },
+})
+
+export const scrollRow = style({
+ display: 'grid',
+ gap: '16px',
+ gridAutoFlow: 'column',
+ gridAutoColumns: 'minmax(84vw, 1fr)',
+ overflowX: 'auto',
+ paddingBottom: '8px',
+ scrollSnapType: 'x mandatory',
+ selectors: {
+ '&::-webkit-scrollbar': {
+ height: '10px',
+ },
+ '&::-webkit-scrollbar-thumb': {
+ background: theme.colors.border,
+ borderRadius: '999px',
+ },
+ },
+ '@media': {
+ 'screen and (min-width: 640px)': {
+ gridAutoColumns: 'minmax(280px, 1fr)',
+ },
+ 'screen and (min-width: 1080px)': {
+ gridAutoColumns: 'minmax(280px, 320px)',
+ },
+ },
+})
+
+export const coiningCard = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '16px',
+ minHeight: '320px',
+ scrollSnapAlign: 'start',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '18px',
+ boxShadow: 'none',
+ minWidth: 0,
+ color: 'inherit',
+ textDecoration: 'none',
+ transition: 'border-color 0.15s ease, background-color 0.15s ease',
+ selectors: {
+ '&:hover': {
+ borderColor: softBlueBorder,
+ backgroundColor: softBlueBackground,
+ cursor: 'pointer',
+ },
+ '&:focus-visible': focusRing,
+ },
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '360px',
+ },
+ },
+})
+
+export const coiningPreview = style({
+ position: 'relative',
+ minHeight: '180px',
+ borderRadius: '10px',
+ overflow: 'hidden',
+ padding: '16px',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ border: standardBorderThin,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '220px',
+ },
+ },
+})
+
+export const coiningPreviewSurfaceA = style({ background: theme.colors.accent })
+export const coiningPreviewSurfaceB = style({ background: theme.colors.warning })
+export const coiningPreviewSurfaceC = style({ background: theme.colors.positive })
+export const coiningPreviewSurfaceD = style({ background: theme.colors.background2 })
+
+export const coiningPreviewTop = style({
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ gap: '12px',
+})
+
+export const coiningPreviewMark = style({
+ alignSelf: 'flex-start',
+ display: 'inline-flex',
+ borderRadius: '999px',
+ background: theme.colors.background1,
+ border: standardBorderThin,
+ padding: '6px 10px',
+ fontSize: '12px',
+ fontWeight: 700,
+ color: theme.colors.text1,
+})
+
+export const coiningNetworkBadge = style({
+ width: '34px',
+ height: '34px',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+})
+
+export const coiningPreviewTitle = style({
+ maxWidth: '12ch',
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '26px',
+ lineHeight: 0.95,
+ color: theme.colors.text1,
+ overflowWrap: 'anywhere',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '32px',
+ },
+ },
+})
+
+export const coiningMeta = style({
+ display: 'grid',
+ gap: '6px',
+ flex: 1,
+})
+
+export const coiningTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '22px',
+ lineHeight: 1.05,
+ color: theme.colors.text1,
+})
+
+export const mutedText = style({
+ fontSize: '14px',
+ lineHeight: 1.6,
+ color: theme.colors.text3,
+})
+
+export const amountPill = style({
+ display: 'inline-flex',
+ width: 'fit-content',
+ borderRadius: '999px',
+ padding: '7px 12px',
+ background: theme.colors.background2,
+ color: theme.colors.text1,
+ border: standardBorderThin,
+ fontSize: '13px',
+ fontWeight: 700,
+})
+
+export const droposalList = style({
+ display: 'grid',
+ gap: '14px',
+})
+
+export const droposalCard = style({
+ display: 'grid',
+ gap: '14px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '18px',
+ boxShadow: 'none',
+ color: 'inherit',
+ textDecoration: 'none',
+ transition: 'border-color 0.15s ease, background-color 0.15s ease',
+ selectors: {
+ '&:hover': {
+ borderColor: softBlueBorder,
+ backgroundColor: softBlueBackground,
+ cursor: 'pointer',
+ },
+ '&:focus-visible': focusRing,
+ },
+ '@media': {
+ 'screen and (min-width: 880px)': {
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
+ alignItems: 'center',
+ },
+ },
+})
+
+export const droposalMeta = style({
+ display: 'flex',
+ flexWrap: 'wrap',
+ gap: '10px',
+ alignItems: 'center',
+})
+
+export const statusBadge = style({
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ minWidth: '88px',
+ borderRadius: '999px',
+ border: standardBorderThin,
+ padding: '7px 12px',
+ fontSize: '12px',
+ fontWeight: 700,
+ background: 'transparent',
+})
+
+export const daoBadge = style({
+ background: '#F7F3E8',
+ color: '#4F4738',
+ borderColor: theme.colors.text1,
+})
+
+export const droposalTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '20px',
+ lineHeight: 1.05,
+ color: theme.colors.text1,
+ overflowWrap: 'anywhere',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '24px',
+ },
+ },
+})
+
+export const droposalSummary = style({
+ fontSize: '15px',
+ lineHeight: 1.65,
+ color: theme.colors.text3,
+ maxWidth: '72ch',
+})
+
+export const droposalAside = style({
+ display: 'grid',
+ gap: '12px',
+ justifyItems: 'start',
+ '@media': {
+ 'screen and (min-width: 880px)': {
+ justifyItems: 'end',
+ textAlign: 'right',
+ },
+ },
+})
+
+export const stepsGrid = style({
+ display: 'grid',
+ gap: '16px',
+ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',
+ '@media': {
+ 'screen and (min-width: 860px)': {
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
+ },
+ },
+})
+
+export const stepCard = style({
+ minHeight: '170px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '22px',
+ boxShadow: 'none',
+})
+
+export const stepHeader = style({
+ display: 'flex',
+ alignItems: 'flex-start',
+ gap: '16px',
+})
+
+export const stepMarker = style({
+ display: 'inline-flex',
+ minWidth: '48px',
+ height: '48px',
+ padding: '0 16px',
+ borderRadius: '10px',
+ alignItems: 'center',
+ justifyContent: 'center',
+ background: theme.colors.focusRing,
+ border: 'none',
+ color: theme.colors.background1,
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '18px',
+ flexShrink: 0,
+})
+
+export const stepTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '28px',
+ lineHeight: 1.02,
+ color: theme.colors.text1,
+})
+
+export const stepBody = style({
+ marginTop: '14px',
+ fontSize: '15px',
+ lineHeight: 1.65,
+ color: theme.colors.text3,
+})
+
+export const valueGrid = style({
+ display: 'grid',
+ gap: '16px',
+ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',
+ '@media': {
+ 'screen and (min-width: 700px)': {
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ },
+ 'screen and (min-width: 1080px)': {
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
+ },
+ },
+})
+
+export const featureStrip = style({
+ position: 'relative',
+ display: 'grid',
+ width: '100%',
+ maxWidth: '100%',
+ alignSelf: 'stretch',
+ gap: '18px',
+ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',
+ '@media': {
+ 'screen and (min-width: 700px)': {
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ },
+ 'screen and (min-width: 1080px)': {
+ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
+ gap: '24px',
+ },
+ },
+})
+
+export const featureItem = style({
+ display: 'grid',
+ gridTemplateColumns: '42px minmax(0, 1fr)',
+ gap: '12px',
+ alignItems: 'center',
+ minWidth: 0,
+ '@media': {
+ 'screen and (min-width: 700px)': {
+ gridTemplateColumns: '42px minmax(0, 1fr)',
+ justifyItems: 'stretch',
+ alignItems: 'center',
+ gap: '12px',
+ },
+ },
+})
+
+export const featureIconBadge = style({
+ position: 'relative',
+ zIndex: 1,
+ width: '42px',
+ height: '42px',
+ borderRadius: '14px',
+ background: theme.colors.background2,
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: theme.colors.text1,
+})
+
+globalStyle(`${featureIconBadge} svg`, {
+ width: '20px',
+ height: '20px',
+ display: 'block',
+})
+
+export const featureLabel = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '17px',
+ lineHeight: 1.2,
+ fontWeight: 700,
+ color: theme.colors.text1,
+ overflowWrap: 'anywhere',
+})
+
+export const useCaseGrid = style({
+ display: 'grid',
+ width: '100%',
+ maxWidth: '100%',
+ alignSelf: 'stretch',
+ gap: '16px',
+ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',
+ '@media': {
+ 'screen and (min-width: 640px)': {
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ },
+ 'screen and (min-width: 900px)': {
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
+ },
+ 'screen and (min-width: 1180px)': {
+ gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
+ },
+ },
+})
+
+export const valueCard = style({
+ minHeight: '150px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '20px',
+ boxShadow: 'none',
+})
+
+export const compactValueCard = style({
+ minHeight: '96px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '16px',
+ boxShadow: 'none',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '10px',
+ minWidth: 0,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ minHeight: '108px',
+ padding: '18px',
+ },
+ },
+})
+
+export const valueTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '24px',
+ lineHeight: 1.05,
+ color: theme.colors.text1,
+})
+
+export const compactValueTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '18px',
+ lineHeight: 1.2,
+ color: theme.colors.text1,
+ display: '-webkit-box',
+ overflow: 'hidden',
+ WebkitLineClamp: 3,
+ WebkitBoxOrient: 'vertical',
+})
+
+export const compactValueEmoji = style({
+ fontSize: '28px',
+ lineHeight: 1,
+})
+
+export const compactValueImage = style({
+ width: '32px',
+ height: '32px',
+ objectFit: 'contain',
+ display: 'block',
+})
+
+export const activityList = style({
+ display: 'grid',
+ gap: '14px',
+})
+
+export const activityCard = style({
+ display: 'grid',
+ gap: '10px',
+ borderRadius: '12px',
+ border: standardBorder,
+ background: theme.colors.background1,
+ padding: '18px',
+ '@media': {
+ 'screen and (min-width: 820px)': {
+ gridTemplateColumns: '140px minmax(0, 1fr)',
+ alignItems: 'start',
+ gap: '18px',
+ },
+ },
+})
+
+export const activityMeta = style({
+ display: 'inline-flex',
+ width: 'fit-content',
+ borderRadius: '999px',
+ padding: '6px 10px',
+ fontSize: '12px',
+ fontWeight: 700,
+ background: theme.colors.background2,
+ color: theme.colors.text3,
+ border: standardBorderThin,
+})
+
+export const activityTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '22px',
+ lineHeight: 1.08,
+ color: theme.colors.text1,
+})
+
+export const finalCta = style({
+ position: 'relative',
+ overflow: 'hidden',
+ borderRadius: '16px',
+ border: standardBorder,
+ background: theme.colors.background2,
+ color: theme.colors.text1,
+ padding: '30px 22px',
+ boxShadow: 'none',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ padding: '40px 34px',
+ },
+ },
+})
+
+export const finalCtaGlow = style({
+ position: 'absolute',
+ inset: 0,
+ pointerEvents: 'none',
+ background: 'transparent',
+})
+
+export const finalCtaContent = style({
+ position: 'relative',
+ zIndex: 1,
+ display: 'grid',
+ gap: '24px',
+ alignItems: 'center',
+ minWidth: 0,
+ '@media': {
+ 'screen and (min-width: 980px)': {
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
+ gap: '32px',
+ },
+ },
+})
+
+export const finalCtaTitle = style({
+ fontFamily: 'ptRoot, sans-serif',
+ fontSize: '32px',
+ lineHeight: 1,
+ whiteSpace: 'normal',
+ overflowWrap: 'anywhere',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ fontSize: '52px',
+ whiteSpace: 'nowrap',
+ overflowWrap: 'normal',
+ },
+ },
+})
+
+export const finalChecklist = style({
+ marginTop: '12px',
+ marginBottom: 0,
+ padding: 0,
+ listStyle: 'none',
+ display: 'grid',
+ gap: '10px',
+})
+
+export const finalChecklistItem = style({
+ display: 'flex',
+ alignItems: 'flex-start',
+ gap: '10px',
+ fontSize: '15px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ alignItems: 'center',
+ fontSize: '17px',
+ lineHeight: 1.6,
+ },
+ },
+})
+
+export const finalChecklistMarker = style({
+ minWidth: '24px',
+ color: theme.colors.text1,
+ fontWeight: 700,
+})
+
+export const finalActions = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '12px',
+ alignItems: 'stretch',
+ '@media': {
+ 'screen and (min-width: 768px)': {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ 'screen and (min-width: 980px)': {
+ justifyContent: 'flex-end',
+ },
+ },
+})
+
+export const aboutCtaButton = style({
+ borderRadius: '999px',
+ transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease',
+ selectors: {
+ '&:not([disabled]):hover': {
+ borderColor: theme.colors.focusRing,
+ backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 14%, ${theme.colors.background1})`,
+ },
+ '&:focus-visible': focusRing,
+ },
+})
+
+globalStyle(`${aboutCtaButton}.zord-button-primary:not([disabled]):hover`, {
+ borderColor: theme.colors.focusRing,
+ backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 82%, ${theme.colors.text1})`,
+ color: theme.colors.background1,
+})
+
+globalStyle(`${aboutCtaButton}.zord-button-outline:not([disabled]):hover`, {
+ borderColor: theme.colors.focusRing,
+ backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 14%, ${theme.colors.background1})`,
+ color: theme.colors.text1,
+})
+
+globalStyle(`${aboutCtaButton}.zord-button-ghost:not([disabled]):hover`, {
+ borderColor: theme.colors.focusRing,
+ backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 16%, ${theme.colors.background1})`,
+ color: theme.colors.text1,
+})
+
+export const subLink = style({
+ color: theme.colors.text3,
+ fontSize: '14px',
+ fontWeight: 700,
+ textDecoration: 'none',
+ width: 'fit-content',
+ selectors: {
+ '&:hover': {
+ color: theme.colors.text1,
+ },
+ '&:focus-visible': focusRing,
+ },
+})
+
+export const heroSubLink = style({
+ color: theme.colors.text3,
+ fontSize: '14px',
+ fontWeight: 700,
+ textDecoration: 'none',
+ selectors: {
+ '&:hover': {
+ color: theme.colors.text1,
+ },
+ '&:focus-visible': focusRing,
+ },
+})
+
+export const helperText = style({
+ fontSize: '14px',
+ lineHeight: 1.55,
+ color: theme.colors.text3,
+})
+
+export const governanceCopy = style({
+ maxWidth: '720px',
+ fontSize: '17px',
+ lineHeight: 1.6,
+ color: theme.colors.text3,
+})
+
+export const governanceLink = style({
+ color: theme.colors.focusRing,
+ textDecoration: 'underline',
+})
+
+globalStyle(
+ `html[data-theme-mode="dark"] ${page} :is(${sectionTitle}, ${heroTitle}, ${montageValue}, ${heroFooterValue}, ${statValue}, ${tabButton}, ${activeTabButton}, ${mobileStatsHeading}, ${daoName}, ${daoSignalValue}, ${coiningTitle}, ${amountPill}, ${droposalTitle}, ${stepTitle}, ${featureItem})`,
+ {
+ color: theme.colors.text1,
+ }
+)
+
+globalStyle(
+ `html[data-theme-mode="dark"] ${page} :is(${sectionCopy}, ${introCopyNoWrap}, ${heroText}, ${heroHighlight}, ${montageBody}, ${statLabel}, ${statDetail}, ${sectionInlineCopy}, ${daoDescription}, ${daoSignalLabel}, ${mutedText}, ${droposalSummary}, ${stepBody}, ${subLink}, ${heroSubLink}, ${helperText}, ${governanceCopy})`,
+ {
+ color: theme.colors.text3,
+ }
+)
+
+globalStyle(`html[data-theme-mode="dark"] ${page} :is(${heroHighlightDot})`, {
+ background: theme.colors.text1,
+})
+
+globalStyle(
+ `html[data-theme-mode="dark"] ${page} ${aboutCtaButton}:not([disabled]):hover`,
+ {
+ borderColor: theme.colors.focusRing,
+ backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 30%, ${theme.colors.background1})`,
+ color: theme.colors.text1,
+ }
+)
+
+globalStyle(
+ `html[data-theme-mode="dark"] ${page} ${aboutCtaButton}.zord-button-primary:not([disabled]):hover`,
+ {
+ borderColor: theme.colors.focusRing,
+ backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 70%, ${theme.colors.text1})`,
+ color: theme.colors.background1,
+ }
+)
+
+globalStyle(`html[data-theme-mode="dark"] ${page} :is(${daoMiniCard}, ${stepCard})`, {
+ background: theme.colors.background2,
+})
+
+globalStyle(`html[data-theme-mode="dark"] ${page} :is(${coiningPreviewTitle})`, {
+ color: theme.colors.background1,
+})
+
+globalStyle(`html[data-theme-mode="dark"] ${logoMarqueeTrack}::before`, {
+ background:
+ 'linear-gradient(90deg, rgba(31, 32, 36, 1) 0%, rgba(31, 32, 36, 0) 10%, rgba(31, 32, 36, 0) 90%, rgba(31, 32, 36, 1) 100%)',
+})
diff --git a/apps/web/src/modules/about/AboutPage.tsx b/apps/web/src/modules/about/AboutPage.tsx
new file mode 100644
index 000000000..a22209557
--- /dev/null
+++ b/apps/web/src/modules/about/AboutPage.tsx
@@ -0,0 +1,149 @@
+import { useChainStore } from '@buildeross/stores'
+import { Box } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ centeredImage,
+ centeredImageWrap,
+ container,
+ governanceCopy,
+ governanceLink,
+ page,
+ section,
+} from './AboutPage.css'
+import { AboutFinalCta } from './components/AboutFinalCta'
+import { AboutHero } from './components/AboutHero'
+import { CoiningHighlightsSection } from './components/CoiningHighlightsSection'
+import { DroposalHighlightsSection } from './components/DroposalHighlightsSection'
+import { FeaturedDaoSection } from './components/FeaturedDaoSection'
+import { ProposalHighlightsSection } from './components/ProposalHighlightsSection'
+import { SectionIntro } from './components/SectionIntro'
+import { WhatIsBuilderSection } from './components/WhatIsBuilderSection'
+import { WhatYouCanBuildSection } from './components/WhatYouCanBuildSection'
+import { WhyBuilderSection } from './components/WhyBuilderSection'
+import { heroHighlights } from './data'
+import { useAboutDaoTabs } from './useAboutDaoTabs'
+import { useAboutShowcase } from './useAboutShowcase'
+import { useAboutSnapshotStats } from './useAboutSnapshotStats'
+
+export const AboutPageView: React.FC = () => {
+ const chain = useChainStore((x) => x.chain)
+ const { data: tabsData, isLoading } = useAboutDaoTabs(chain.slug)
+ const { data: snapshotData } = useAboutSnapshotStats()
+ const { data: showcaseData } = useAboutShowcase()
+ const heroLogos = React.useMemo(() => {
+ if (!tabsData) return []
+
+ const seen = new Set()
+
+ return Object.values(tabsData)
+ .flat()
+ .filter((dao) => dao.recentAuctionImage)
+ .filter((dao) => {
+ if (!dao.recentAuctionImage || seen.has(dao.recentAuctionImage)) return false
+ seen.add(dao.recentAuctionImage)
+ return true
+ })
+ .slice(0, 12)
+ .map((dao) => ({
+ id: dao.id,
+ name: dao.name,
+ imageUrl: dao.recentAuctionImage as string,
+ }))
+ }, [tabsData])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This public good tooling and the Nouns Builder Protocol are maintained and
+ governed by{' '}
+
+ Builder DAO
+
+ . Learn more about the DAO's vision and mission{' '}
+
+ here
+
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/AboutFinalCta.tsx b/apps/web/src/modules/about/components/AboutFinalCta.tsx
new file mode 100644
index 000000000..f1d8e28f1
--- /dev/null
+++ b/apps/web/src/modules/about/components/AboutFinalCta.tsx
@@ -0,0 +1,80 @@
+import { Box, Button, Text } from '@buildeross/zord'
+import Link from 'next/link'
+import React from 'react'
+
+import {
+ aboutCtaButton,
+ finalActions,
+ finalChecklist,
+ finalChecklistItem,
+ finalChecklistMarker,
+ finalCta,
+ finalCtaContent,
+ finalCtaGlow,
+ finalCtaTitle,
+} from '../AboutPage.css'
+
+export const AboutFinalCta: React.FC = () => {
+ return (
+
+
+
+
+ Start your DAO today
+
+
+
+ 1.
+
+ Upload your art
+
+
+
+ 2.
+
+ Set your parameters
+
+
+
+ 3.
+
+ Launch your first auction
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/AboutHero.tsx b/apps/web/src/modules/about/components/AboutHero.tsx
new file mode 100644
index 000000000..df786a2b2
--- /dev/null
+++ b/apps/web/src/modules/about/components/AboutHero.tsx
@@ -0,0 +1,140 @@
+import { Box, Button, Text } from '@buildeross/zord'
+import Link from 'next/link'
+import React from 'react'
+
+import {
+ aboutCtaButton,
+ aboutDarkOnly,
+ aboutLightOnly,
+ hero,
+ heroActions,
+ heroCopy,
+ heroHighlight,
+ heroHighlightDot,
+ heroHighlightList,
+ heroPanel,
+ heroPanelGlow,
+ heroText,
+ heroTitle,
+ heroVennImage,
+ heroVennWrap,
+ logoMarquee,
+ logoMarqueeImage,
+ logoMarqueeInner,
+ logoMarqueeItem,
+ logoMarqueeTrack,
+ montageBody,
+ montageCard,
+ montageFooter,
+ montageGrid,
+ montagePrimary,
+ montageSide,
+ montageValue,
+} from '../AboutPage.css'
+
+type AboutHeroProps = {
+ heroHighlights: string[]
+ heroLogos: Array<{
+ id: string
+ name: string
+ imageUrl: string
+ }>
+}
+
+export const AboutHero: React.FC = ({ heroHighlights, heroLogos }) => {
+ const marqueeLogos = heroLogos.length > 0 ? [...heroLogos, ...heroLogos] : []
+
+ return (
+
+
+
+ The easiest way to build communities onchain
+
+
+ Nouns Builder lets anyone launch a DAO where membership, funding, content and
+ governance all happen in one fully transparent, onchain system.
+
+
+
+ {heroHighlights.map((item) => (
+
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {`Launch.\nFund.\nCollaborate.`}
+
+ The onchain operating system for decentralized communities.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {marqueeLogos.map((logo, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx
new file mode 100644
index 000000000..f468c13aa
--- /dev/null
+++ b/apps/web/src/modules/about/components/CoiningCard.tsx
@@ -0,0 +1,60 @@
+import { Box, Text } from '@buildeross/zord'
+import Link from 'next/link'
+import React from 'react'
+
+import {
+ coiningCard,
+ coiningMeta,
+ coiningNetworkBadge,
+ coiningPreview,
+ coiningPreviewMark,
+ coiningPreviewTitle,
+ coiningPreviewTop,
+ coiningTitle,
+ daoChainBadgeImage,
+ mutedText,
+} from '../AboutPage.css'
+import { CoiningHighlight } from '../types'
+import { getChainLogoSrc } from '../utils'
+
+type CoiningCardProps = {
+ item: CoiningHighlight
+}
+
+export const CoiningCard: React.FC = ({ item }) => {
+ const chainLogoSrc = getChainLogoSrc(item.chainLabel)
+
+ return (
+
+
+
+ {item.amount}
+ {chainLogoSrc ? (
+
+
+
+ ) : (
+ {item.chainLabel}
+ )}
+
+ {item.previewLabel}
+
+
+
+ {item.title}
+
+ By {item.creator} for {item.dao}
+
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/CoiningHighlightsSection.tsx b/apps/web/src/modules/about/components/CoiningHighlightsSection.tsx
new file mode 100644
index 000000000..c3ecec7c3
--- /dev/null
+++ b/apps/web/src/modules/about/components/CoiningHighlightsSection.tsx
@@ -0,0 +1,34 @@
+import { Box } from '@buildeross/zord'
+import React from 'react'
+
+import { scrollRow } from '../AboutPage.css'
+import { coiningHighlights } from '../data'
+import { CoiningHighlight } from '../types'
+import { CoiningCard } from './CoiningCard'
+import { SectionIntro } from './SectionIntro'
+
+type CoiningHighlightsSectionProps = {
+ items?: CoiningHighlight[]
+}
+
+export const CoiningHighlightsSection: React.FC = ({
+ items,
+}) => {
+ const highlights = items?.length ? items : coiningHighlights
+
+ return (
+
+
+
+
+ {highlights.map((item) => (
+
+ ))}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx
new file mode 100644
index 000000000..217b0ab3d
--- /dev/null
+++ b/apps/web/src/modules/about/components/DaoCard.tsx
@@ -0,0 +1,80 @@
+import { FallbackImage } from '@buildeross/ui'
+import { Box, Flex, Text } from '@buildeross/zord'
+import Link from 'next/link'
+import React from 'react'
+
+import {
+ daoAvatar,
+ daoAvatarImage,
+ daoAvatarSurfaceA,
+ daoAvatarSurfaceB,
+ daoAvatarSurfaceC,
+ daoAvatarSurfaceD,
+ daoCard,
+ daoChainBadge,
+ daoChainBadgeImage,
+ daoDescription,
+ daoIdentity,
+ daoName,
+ daoSignal,
+ daoSignalLabel,
+ daoSignalValue,
+ daoTop,
+} from '../AboutPage.css'
+import { AboutDao } from '../types'
+import { getChainLogoSrc, toPlainText } from '../utils'
+
+type DaoCardProps = {
+ dao: AboutDao
+}
+
+export const DaoCard: React.FC = ({ dao }) => {
+ const plainDescription = toPlainText(dao.description)
+ const chainLogoSrc = getChainLogoSrc(dao.chainName) || dao.chainIcon
+
+ const avatarSurfaceClass = [
+ daoAvatarSurfaceA,
+ daoAvatarSurfaceB,
+ daoAvatarSurfaceC,
+ daoAvatarSurfaceD,
+ ][Number(dao.id.replace(/\D/g, '')) % 4 || 0]
+
+ return (
+
+
+
+
+ {dao.imageUrl ? (
+
+ ) : (
+ dao.initials
+ )}
+
+
+ {dao.name}
+ {plainDescription}
+
+
+ {chainLogoSrc ? (
+
+
+
+ ) : null}
+
+
+
+ {dao.signalLabel}
+ {dao.signalValue}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx
new file mode 100644
index 000000000..e9bbdd252
--- /dev/null
+++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx
@@ -0,0 +1,139 @@
+import { getProposalStateColorStyle } from '@buildeross/proposal-ui'
+import { ProposalState } from '@buildeross/sdk/contract'
+import { Box, Text } from '@buildeross/zord'
+import Link from 'next/link'
+import React from 'react'
+
+import {
+ badge,
+ daoBadge,
+ daoChainBadge,
+ daoChainBadgeImage,
+ droposalAside,
+ droposalCard,
+ droposalList,
+ droposalMeta,
+ droposalSummary,
+ droposalTitle,
+ mutedText,
+ statusBadge,
+} from '../AboutPage.css'
+import { dropHighlights } from '../data'
+import { DroposalHighlight } from '../types'
+import { getChainLogoSrc } from '../utils'
+import { SectionIntro } from './SectionIntro'
+
+const statusStyleByType: Record<
+ DroposalHighlight['status'],
+ { borderColor: string; color: string; backgroundColor: string }
+> = {
+ Active: {
+ ...getProposalStateColorStyle(ProposalState.Active),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Active).borderColor} 12%, transparent)`,
+ },
+ Succeeded: {
+ ...getProposalStateColorStyle(ProposalState.Succeeded),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Succeeded).borderColor} 12%, transparent)`,
+ },
+ Queued: {
+ ...getProposalStateColorStyle(ProposalState.Queued),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Queued).borderColor} 12%, transparent)`,
+ },
+ Defeated: {
+ ...getProposalStateColorStyle(ProposalState.Defeated),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Defeated).borderColor} 12%, transparent)`,
+ },
+ Executed: {
+ ...getProposalStateColorStyle(ProposalState.Executed),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Executed).borderColor} 12%, transparent)`,
+ },
+ Trending: {
+ ...getProposalStateColorStyle(ProposalState.Succeeded),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Succeeded).borderColor} 12%, transparent)`,
+ },
+ Live: {
+ ...getProposalStateColorStyle(ProposalState.Succeeded),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Succeeded).borderColor} 12%, transparent)`,
+ },
+ Recent: {
+ ...getProposalStateColorStyle(ProposalState.Active),
+ backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Active).borderColor} 12%, transparent)`,
+ },
+}
+
+type DroposalHighlightsSectionProps = {
+ items?: DroposalHighlight[]
+ eyebrowText?: string
+ title?: string
+ copy?: string
+ linkLabel?: string
+ showStatusBadge?: boolean
+}
+
+export const DroposalHighlightsSection: React.FC = ({
+ items,
+ eyebrowText = 'Drops',
+ title = 'Drops turn releases into onchain distribution',
+ copy = 'Launch collectible drops that turn media, editions, and releases into distribution, ownership, and treasury growth for decentralized communities.',
+ linkLabel = 'View drop',
+ showStatusBadge = true,
+}) => {
+ const highlights = items?.length ? items : dropHighlights
+
+ return (
+
+
+
+
+ {highlights.map((proposal) => {
+ const chainLogoSrc = getChainLogoSrc(proposal.category)
+
+ return (
+
+
+
+ {proposal.dao}
+ {showStatusBadge ? (
+
+ {proposal.status}
+
+ ) : null}
+ {chainLogoSrc ? (
+
+
+
+ ) : (
+ {proposal.category}
+ )}
+
+
+ {proposal.title}
+
+
+ {proposal.summary}
+
+
+
+
+ {proposal.amount}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/EcosystemActivitySection.tsx b/apps/web/src/modules/about/components/EcosystemActivitySection.tsx
new file mode 100644
index 000000000..e1d9a7ac1
--- /dev/null
+++ b/apps/web/src/modules/about/components/EcosystemActivitySection.tsx
@@ -0,0 +1,40 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ activityCard,
+ activityList,
+ activityMeta,
+ activityTitle,
+ mutedText,
+} from '../AboutPage.css'
+import { ecosystemActivity } from '../data'
+import { SectionIntro } from './SectionIntro'
+
+export const EcosystemActivitySection: React.FC = () => {
+ return (
+
+
+
+
+ {ecosystemActivity.map((item) => (
+
+ {item.meta}
+
+
+ {item.title}
+
+
+ {item.detail}
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/EcosystemStatGrid.tsx b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx
new file mode 100644
index 000000000..fe2acab7d
--- /dev/null
+++ b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx
@@ -0,0 +1,56 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ statAccent,
+ statAccentAuction,
+ statAccentDao,
+ statAccentMembers,
+ statAccentProposal,
+ statAccentTokens,
+ statAccentTreasury,
+ statCard,
+ statDetail,
+ statGrid,
+ statLabel,
+ statValue,
+} from '../AboutPage.css'
+import { AboutStat } from '../types'
+
+type EcosystemStatGridProps = {
+ stats: AboutStat[]
+}
+
+export const EcosystemStatGrid: React.FC = ({ stats }) => {
+ const getAccentClass = (id: string) => {
+ switch (id) {
+ case 'daos':
+ return statAccentDao
+ case 'treasury':
+ return statAccentTreasury
+ case 'auctions':
+ return statAccentAuction
+ case 'proposals':
+ return statAccentProposal
+ case 'members':
+ return statAccentMembers
+ case 'tokens':
+ return statAccentTokens
+ default:
+ return statAccentDao
+ }
+ }
+
+ return (
+
+ {stats.map((stat) => (
+
+ {stat.icon}
+ {stat.label}
+ {stat.value}
+ {stat.detail}
+
+ ))}
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/FeaturedDaoSection.tsx b/apps/web/src/modules/about/components/FeaturedDaoSection.tsx
new file mode 100644
index 000000000..c637be2c7
--- /dev/null
+++ b/apps/web/src/modules/about/components/FeaturedDaoSection.tsx
@@ -0,0 +1,88 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ activeTabButton,
+ daoGrid,
+ mobileStatsHeading,
+ sectionInlineCopy,
+ sectionInlineRow,
+ sectionTitle,
+ sectionTitleOnly,
+ statsBlock,
+ tabButton,
+ tabs,
+} from '../AboutPage.css'
+import { AboutDaoTabKey, AboutDaoTabsResponse, AboutStat } from '../types'
+import { DaoCard } from './DaoCard'
+import { EcosystemStatGrid } from './EcosystemStatGrid'
+
+type FeaturedDaoSectionProps = {
+ tabsData?: AboutDaoTabsResponse
+ stats?: AboutStat[]
+ isLoading: boolean
+}
+
+const tabOrder: { id: AboutDaoTabKey; label: string }[] = [
+ { id: 'featured', label: 'Featured' },
+ { id: 'trending', label: 'Trending' },
+ { id: 'new', label: 'New' },
+ { id: 'active', label: 'Active' },
+]
+
+export const FeaturedDaoSection: React.FC = ({
+ tabsData,
+ stats,
+ isLoading: _isLoading,
+}) => {
+ const [tab, setTab] = React.useState('featured')
+
+ const displayedDaos = React.useMemo(() => {
+ return tabsData?.[tab] ?? []
+ }, [tab, tabsData])
+
+ return (
+
+
+ A thriving ecosystem
+
+
+
+
+ Nouns Builder is already powering a growing network of decentralized
+ communities.
+
+
+
+ {tabOrder.map((item) => {
+ const isActive = item.id === tab
+ return (
+
+ )
+ })}
+
+
+
+
+ {displayedDaos.map((dao) => (
+
+ ))}
+
+
+
+ Protocol Stats
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/HowItWorksSection.tsx b/apps/web/src/modules/about/components/HowItWorksSection.tsx
new file mode 100644
index 000000000..4d3d569ae
--- /dev/null
+++ b/apps/web/src/modules/about/components/HowItWorksSection.tsx
@@ -0,0 +1,35 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ stepBody,
+ stepCard,
+ stepHeader,
+ stepMarker,
+ stepsGrid,
+ stepTitle,
+} from '../AboutPage.css'
+import { builderSteps } from '../data'
+import { SectionIntro } from './SectionIntro'
+
+export const HowItWorksSection: React.FC = () => {
+ return (
+
+
+
+
+ {builderSteps.map((step) => (
+
+
+ {step.marker}
+
+ {step.title}
+
+
+ {step.body}
+
+ ))}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/ProposalHighlightsSection.tsx b/apps/web/src/modules/about/components/ProposalHighlightsSection.tsx
new file mode 100644
index 000000000..b494f4a42
--- /dev/null
+++ b/apps/web/src/modules/about/components/ProposalHighlightsSection.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+
+import { proposalHighlights } from '../data'
+import { DroposalHighlight } from '../types'
+import { DroposalHighlightsSection } from './DroposalHighlightsSection'
+
+type ProposalHighlightsSectionProps = {
+ items?: DroposalHighlight[]
+}
+
+export const ProposalHighlightsSection: React.FC = ({
+ items,
+}) => {
+ const highlights = items?.length ? items : proposalHighlights
+
+ return (
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/SectionIntro.tsx b/apps/web/src/modules/about/components/SectionIntro.tsx
new file mode 100644
index 000000000..75aa29d0c
--- /dev/null
+++ b/apps/web/src/modules/about/components/SectionIntro.tsx
@@ -0,0 +1,28 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import { eyebrow, sectionCopy, sectionHeader, sectionTitle } from '../AboutPage.css'
+
+type SectionIntroProps = {
+ eyebrowText: string
+ title: string
+ copy?: string
+ copyClassName?: string
+}
+
+export const SectionIntro: React.FC = ({
+ eyebrowText,
+ title,
+ copy,
+ copyClassName,
+}) => {
+ return (
+
+ {eyebrowText}
+
+ {title}
+
+ {copy ? {copy} : null}
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/WhatIsBuilderSection.tsx b/apps/web/src/modules/about/components/WhatIsBuilderSection.tsx
new file mode 100644
index 000000000..298b88146
--- /dev/null
+++ b/apps/web/src/modules/about/components/WhatIsBuilderSection.tsx
@@ -0,0 +1,43 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ mutedText,
+ stepHeader,
+ stepMarker,
+ valueCard,
+ valueGrid,
+ valueTitle,
+} from '../AboutPage.css'
+import { builderFeatures } from '../data'
+import { SectionIntro } from './SectionIntro'
+
+export const WhatIsBuilderSection: React.FC = () => {
+ return (
+
+
+
+
+ {builderFeatures.map((item) => (
+
+
+
+ {item.marker}
+
+
+ {item.title}
+
+
+
+ {item.body}
+
+
+ ))}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx b/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx
new file mode 100644
index 000000000..0051a388f
--- /dev/null
+++ b/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx
@@ -0,0 +1,62 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ aboutDarkOnly,
+ aboutLightOnly,
+ compactValueCard,
+ compactValueEmoji,
+ compactValueImage,
+ compactValueTitle,
+ useCaseGrid,
+} from '../AboutPage.css'
+import { builderUseCases } from '../data'
+import { SectionIntro } from './SectionIntro'
+
+export const WhatYouCanBuildSection: React.FC = () => {
+ return (
+
+
+
+
+ {builderUseCases.map((item) => (
+
+ {item.id === 'media-collectives' ? (
+ <>
+
+
+ >
+ ) : item.imageSrc ? (
+
+ ) : item.emoji ? (
+ {item.emoji}
+ ) : null}
+
+ {item.title}
+
+
+ ))}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/components/WhyBuilderSection.tsx b/apps/web/src/modules/about/components/WhyBuilderSection.tsx
new file mode 100644
index 000000000..1be68577a
--- /dev/null
+++ b/apps/web/src/modules/about/components/WhyBuilderSection.tsx
@@ -0,0 +1,118 @@
+import { Box, Text } from '@buildeross/zord'
+import React from 'react'
+
+import {
+ featureIconBadge,
+ featureItem,
+ featureLabel,
+ featureStrip,
+ section,
+} from '../AboutPage.css'
+import { builderValueProps } from '../data'
+import type { BuilderValueProp } from '../types'
+import { SectionIntro } from './SectionIntro'
+
+type FeatureIcon = 'rocket' | 'treasury' | 'governance' | 'creative'
+
+const featureIconsById: Record = {
+ 'launch-fast': 'rocket',
+ 'treasury-auctions': 'treasury',
+ 'governance-day-one': 'governance',
+ 'creative-output': 'creative',
+}
+
+const FeatureIconSvg = ({ icon }: { icon: FeatureIcon }) => {
+ switch (icon) {
+ case 'rocket':
+ return (
+
+ )
+ case 'treasury':
+ return (
+
+ )
+ case 'governance':
+ return (
+
+ )
+ case 'creative':
+ return (
+
+ )
+ }
+}
+
+const FeatureItem = ({ item }: { item: BuilderValueProp }) => (
+
+
+
+
+
+ {item.title}
+
+
+)
+
+export const WhyBuilderSection: React.FC = () => {
+ return (
+
+
+
+
+ {builderValueProps.map((item) => (
+
+ ))}
+
+
+ )
+}
diff --git a/apps/web/src/modules/about/data.ts b/apps/web/src/modules/about/data.ts
new file mode 100644
index 000000000..1ea94ba78
--- /dev/null
+++ b/apps/web/src/modules/about/data.ts
@@ -0,0 +1,284 @@
+import {
+ ActivityItem,
+ BuilderFeature,
+ BuilderStep,
+ BuilderValueProp,
+ CoiningHighlight,
+ DroposalHighlight,
+} from './types'
+
+// TODO: Replace seeded showcase data with an aggregated ecosystem endpoint once
+// cross-DAO ranking and coining feeds are available for the public about page.
+export const coiningHighlights: CoiningHighlight[] = [
+ {
+ id: 'studio-coin',
+ title: 'Studio Coin',
+ creator: '0x7d4f...19ae',
+ dao: 'Studio Nouns',
+ chainLabel: 'Base',
+ amount: '$1.8M market cap',
+ href: '/explore?search=studio%20nouns',
+ eyebrow: 'DAO paired coin',
+ surface: 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)',
+ previewLabel: 'Creator pair',
+ },
+ {
+ id: 'camp-coin',
+ title: 'Camp Coin',
+ creator: '0x4ac2...d6bf',
+ dao: 'Camp Nouns',
+ chainLabel: 'Base',
+ amount: '$940K market cap',
+ href: '/explore?search=camp%20nouns',
+ eyebrow: 'DAO paired coin',
+ surface: 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)',
+ previewLabel: 'Content pair',
+ },
+ {
+ id: 'builder-coin',
+ title: 'Builder Coin',
+ creator: '0x8cf4...b2da',
+ dao: 'BuilderDAO',
+ chainLabel: 'Base',
+ amount: '$1.2M market cap',
+ href: '/explore?search=builderdao',
+ eyebrow: 'DAO paired coin',
+ surface: 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)',
+ previewLabel: 'Onchain market',
+ },
+ {
+ id: 'fest-coin',
+ title: 'Fest Coin',
+ creator: '0x1ce9...42f0',
+ dao: 'Nouns Fest',
+ chainLabel: 'Ethereum',
+ amount: '$680K market cap',
+ href: '/explore?search=nouns%20fest',
+ eyebrow: 'DAO paired coin',
+ surface: 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)',
+ previewLabel: 'Content pair',
+ },
+]
+
+export const dropHighlights: DroposalHighlight[] = [
+ {
+ id: 'open-edition-jam-drop',
+ title: 'Open Edition Jam',
+ dao: 'Studio Nouns',
+ amount: 'ETH 18.6 sold',
+ status: 'Live',
+ summary:
+ 'A live edition drop distributing creative work onchain with sales flowing to the DAO treasury.',
+ href: '/explore?search=studio%20nouns',
+ category: 'Base',
+ },
+ {
+ id: 'summer-camp-poster-drop',
+ title: 'Summer Camp Poster Pack',
+ dao: 'Camp Nouns',
+ amount: 'ETH 9.4 sold',
+ status: 'Recent',
+ summary:
+ 'A collectible poster release used to distribute media and fund creative output through Builder.',
+ href: '/explore?search=camp%20nouns',
+ category: 'Base',
+ },
+ {
+ id: 'governance-zine-drop',
+ title: 'Governance Zine 001',
+ dao: 'BuilderDAO',
+ amount: 'ETH 12.1 sold',
+ status: 'Recent',
+ summary:
+ 'An editorial drop that packages governance content into onchain ownership and treasury formation.',
+ href: '/explore?search=builderdao',
+ category: 'Base',
+ },
+ {
+ id: 'signal-radio-drop',
+ title: 'Signal Radio Session',
+ dao: 'Nouns Fest',
+ amount: 'ETH 6.8 sold',
+ status: 'Live',
+ summary:
+ 'A media drop extending event programming into onchain distribution, ownership, and funding.',
+ href: '/explore?search=nouns%20fest',
+ category: 'Ethereum',
+ },
+]
+
+export const proposalHighlights: DroposalHighlight[] = [
+ {
+ id: 'operator-tooling',
+ title: 'Operator tooling sprint for treasury reporting',
+ dao: 'BuilderDAO',
+ amount: '42 voters',
+ status: 'Queued',
+ summary:
+ 'Shared treasury analytics and operator tooling improve governance workflows across Builder DAOs.',
+ href: '/explore?search=builderdao',
+ category: 'Base',
+ },
+ {
+ id: 'festival-grants',
+ title: 'Mini grants for local Nouns Fest activations',
+ dao: 'Nouns Fest',
+ amount: '18 voters',
+ status: 'Succeeded',
+ summary:
+ 'Regional teams get budget for activations, documentation, and post-event publishing.',
+ href: '/explore?search=nouns%20fest',
+ category: 'Ethereum',
+ },
+ {
+ id: 'creator-retainer',
+ title: 'Quarterly creative retainer for media releases',
+ dao: 'Studio Nouns',
+ amount: '24 voters',
+ status: 'Active',
+ summary:
+ 'Sustains ongoing visual output with transparent milestones and release cadence.',
+ href: '/explore?search=studio%20nouns',
+ category: 'Base',
+ },
+ {
+ id: 'builder-residency',
+ title: 'Residency round for new onchain builders',
+ dao: 'Far House',
+ amount: '30 voters',
+ status: 'Defeated',
+ summary:
+ 'Funds a cohort of builders experimenting with coining, drops, and governance flows.',
+ href: '/explore?search=far%20house',
+ category: 'Optimism',
+ },
+ {
+ id: 'archive-pipeline',
+ title: 'Archive and indexing pipeline for DAO memory',
+ dao: 'Prop House',
+ amount: '12 voters',
+ status: 'Executed',
+ summary:
+ 'Creates searchable records for proposal outcomes, experiments, and funded work.',
+ href: '/explore?search=prop%20house',
+ category: 'Ethereum',
+ },
+]
+
+export const builderSteps: BuilderStep[] = [
+ {
+ id: 'launch',
+ title: 'Launch your DAO',
+ body: 'Spin up a DAO with auctions, governance, and treasury built in.',
+ marker: '1',
+ },
+ {
+ id: 'grow',
+ title: 'Grow your treasury',
+ body: 'Use auctions and coining to generate ongoing funding.',
+ marker: '2',
+ },
+ {
+ id: 'fund',
+ title: 'Fund ideas',
+ body: 'Use proposals to allocate capital to builders, events, and experiments.',
+ marker: '3',
+ },
+]
+
+export const builderFeatures: BuilderFeature[] = [
+ {
+ id: 'auctions',
+ marker: '1',
+ title: 'Auctions bring in new members and fund the treasury',
+ body: 'Ownership and capital contribution in one simple mechanic.',
+ },
+ {
+ id: 'coining',
+ marker: '2',
+ title: 'Members participate in collaborative content creation',
+ body: 'Create content as coins and droposals to turn media into funding utilizing built in distribution methods.',
+ },
+ {
+ id: 'proposals',
+ marker: '3',
+ title: 'The treasury funds ideas',
+ body: 'Members vote on proposals to fund work and allocate capital, all transparently onchain.',
+ },
+]
+
+export const builderValueProps: BuilderValueProp[] = [
+ {
+ id: 'launch-fast',
+ title: 'Launch a DAO in minutes',
+ },
+ {
+ id: 'treasury-auctions',
+ title: 'Built-in treasury formation through auctions',
+ },
+ {
+ id: 'governance-day-one',
+ title: 'Governance included from day one',
+ },
+ {
+ id: 'creative-output',
+ title: 'Native support for creative output',
+ },
+]
+
+export const ecosystemActivity: ActivityItem[] = [
+ {
+ id: 'launches',
+ title: 'New launches keep expanding the graph',
+ detail:
+ 'Fresh DAOs continue showing up with tighter scopes: media, local communities, product teams, and events.',
+ meta: 'Recent launches',
+ tone: 'launch',
+ },
+ {
+ id: 'coining',
+ title: 'Coining is broadening what a DAO can publish',
+ detail:
+ 'Posts are becoming funding surfaces for essays, visuals, audio, and experiments instead of just announcements.',
+ meta: 'Creator momentum',
+ tone: 'coin',
+ },
+ {
+ id: 'droposals',
+ title: 'Droposals are routing treasury into clearer outcomes',
+ detail:
+ 'Teams are packaging work with tighter scope, deliverables, and faster governance cycles.',
+ meta: 'Governance patterns',
+ tone: 'governance',
+ },
+ {
+ id: 'protocol',
+ title: 'Builder keeps shipping new primitives',
+ detail:
+ 'The protocol is evolving around creation flows, treasury tooling, content, and governance ergonomics.',
+ meta: 'Protocol motion',
+ tone: 'protocol',
+ },
+]
+
+export const heroHighlights = [
+ 'Each new member joins through an auction.',
+ 'Every auction funds a shared treasury.',
+ 'Token holders propose and vote on how that capital is used.',
+]
+
+export const builderUseCases: BuilderValueProp[] = [
+ {
+ id: 'media-collectives',
+ title: 'Community owned brands',
+ imageSrc: '/noggles-square.svg',
+ },
+ { id: 'creative-communities', title: 'Creative and media collectives', emoji: '🎨' },
+ { id: 'events-local-groups', title: 'Local and event-based groups', emoji: '🎪' },
+ { id: 'protocol-teams', title: 'Protocol teams', emoji: '⚙️' },
+ {
+ id: 'experimental-governance',
+ title: 'Subculture communities',
+ emoji: '🛹',
+ },
+]
diff --git a/apps/web/src/modules/about/index.ts b/apps/web/src/modules/about/index.ts
new file mode 100644
index 000000000..3564b71bf
--- /dev/null
+++ b/apps/web/src/modules/about/index.ts
@@ -0,0 +1 @@
+export * from './AboutPage'
diff --git a/apps/web/src/modules/about/types.ts b/apps/web/src/modules/about/types.ts
new file mode 100644
index 000000000..6798eb298
--- /dev/null
+++ b/apps/web/src/modules/about/types.ts
@@ -0,0 +1,108 @@
+export type AboutStat = {
+ id: string
+ label: string
+ value: string
+ detail: string
+ icon: string
+}
+
+export type DaoCategory = 'featured' | 'trending' | 'new' | 'active'
+
+export type AboutDao = {
+ id: string
+ name: string
+ description: string
+ signalLabel: string
+ signalValue: string
+ href: string
+ badge: string
+ initials: string
+ imageUrl?: string | null
+ recentAuctionImage?: string | null
+ chainIcon?: string | null
+ chainName?: string | null
+ category: DaoCategory
+}
+
+export type CoiningHighlight = {
+ id: string
+ title: string
+ creator: string
+ dao: string
+ chainLabel: string
+ amount: string
+ href: string
+ eyebrow: string
+ surface: string
+ previewLabel: string
+}
+
+export type DroposalStatus =
+ | 'Active'
+ | 'Succeeded'
+ | 'Queued'
+ | 'Defeated'
+ | 'Executed'
+ | 'Trending'
+ | 'Live'
+ | 'Recent'
+
+export type DroposalHighlight = {
+ id: string
+ title: string
+ dao: string
+ amount: string
+ status: DroposalStatus
+ summary: string
+ href: string
+ category: string
+}
+
+export type BuilderStep = {
+ id: string
+ title: string
+ body: string
+ marker: string
+}
+
+export type BuilderValueProp = {
+ id: string
+ title: string
+ body?: string
+ emoji?: string
+ imageSrc?: string
+}
+
+export type ActivityItem = {
+ id: string
+ title: string
+ detail: string
+ meta: string
+ tone: 'launch' | 'coin' | 'governance' | 'protocol'
+}
+
+export type AboutDaoTabKey = 'featured' | 'trending' | 'new' | 'active'
+
+export type AboutDaoTabsResponse = {
+ featured: AboutDao[]
+ trending: AboutDao[]
+ new: AboutDao[]
+ active: AboutDao[]
+}
+
+export type AboutSnapshotResponse = {
+ stats: AboutStat[]
+}
+
+export type AboutShowcaseResponse = {
+ coining: CoiningHighlight[]
+ drops: DroposalHighlight[]
+ proposals: DroposalHighlight[]
+}
+
+export type BuilderFeature = {
+ id: string
+ marker: string
+ title: string
+ body: string
+}
diff --git a/apps/web/src/modules/about/useAboutDaoTabs.ts b/apps/web/src/modules/about/useAboutDaoTabs.ts
new file mode 100644
index 000000000..b74b20e26
--- /dev/null
+++ b/apps/web/src/modules/about/useAboutDaoTabs.ts
@@ -0,0 +1,60 @@
+import { SWR_KEYS } from '@buildeross/constants/swrKeys'
+import useSWR from 'swr'
+
+import { AboutDaoTabsResponse } from './types'
+
+type HttpError = Error & { status?: number; body?: unknown }
+
+const fetcher = async (
+ [, network]: readonly [string, string],
+ { signal }: { signal?: AbortSignal } = {}
+): Promise => {
+ const response = await fetch(
+ `/api/about/dao-tabs?network=${encodeURIComponent(network)}`,
+ {
+ signal,
+ headers: { Accept: 'application/json' },
+ }
+ )
+
+ const text = await response.text()
+ let body: unknown = {}
+
+ try {
+ body = text ? JSON.parse(text) : {}
+ } catch {
+ body = text
+ }
+
+ if (!response.ok) {
+ const err: HttpError = new Error(
+ typeof body === 'object' && body && 'error' in body
+ ? String((body as { error?: string }).error || response.statusText)
+ : response.statusText
+ )
+ err.status = response.status
+ err.body = body
+ throw err
+ }
+
+ return body as AboutDaoTabsResponse
+}
+
+export const useAboutDaoTabs = (network?: string) => {
+ const swrKey = network ? ([SWR_KEYS.EXPLORE, network] as const) : null
+
+ const { data, error, isLoading, isValidating } = useSWR<
+ AboutDaoTabsResponse,
+ HttpError
+ >(swrKey, fetcher, {
+ revalidateOnFocus: false,
+ revalidateOnReconnect: true,
+ dedupingInterval: 60000,
+ })
+
+ return {
+ data,
+ error,
+ isLoading: !!swrKey && (isLoading || isValidating),
+ }
+}
diff --git a/apps/web/src/modules/about/useAboutShowcase.ts b/apps/web/src/modules/about/useAboutShowcase.ts
new file mode 100644
index 000000000..a92849c61
--- /dev/null
+++ b/apps/web/src/modules/about/useAboutShowcase.ts
@@ -0,0 +1,44 @@
+import useSWR from 'swr'
+
+import { AboutShowcaseResponse } from './types'
+
+type HttpError = Error & { status?: number; body?: unknown }
+
+const fetcher = async (
+ [url]: readonly [string],
+ { signal }: { signal?: AbortSignal } = {}
+): Promise => {
+ const response = await fetch(url, {
+ signal,
+ headers: { Accept: 'application/json' },
+ })
+
+ const text = await response.text()
+ const body = text ? JSON.parse(text) : {}
+
+ if (!response.ok) {
+ const err: HttpError = new Error(body?.error || response.statusText)
+ err.status = response.status
+ err.body = body
+ throw err
+ }
+
+ return body as AboutShowcaseResponse
+}
+
+export const useAboutShowcase = () => {
+ const { data, error, isLoading, isValidating } = useSWR<
+ AboutShowcaseResponse,
+ HttpError
+ >(['/api/about/showcase'] as const, fetcher, {
+ revalidateOnFocus: false,
+ revalidateOnReconnect: true,
+ dedupingInterval: 60000,
+ })
+
+ return {
+ data,
+ error,
+ isLoading: isLoading || isValidating,
+ }
+}
diff --git a/apps/web/src/modules/about/useAboutSnapshotStats.ts b/apps/web/src/modules/about/useAboutSnapshotStats.ts
new file mode 100644
index 000000000..e1edd82d1
--- /dev/null
+++ b/apps/web/src/modules/about/useAboutSnapshotStats.ts
@@ -0,0 +1,54 @@
+import useSWR from 'swr'
+
+import { AboutSnapshotResponse } from './types'
+
+type HttpError = Error & { status?: number; body?: unknown }
+
+const fetcher = async (
+ [url]: readonly [string],
+ { signal }: { signal?: AbortSignal } = {}
+): Promise => {
+ const response = await fetch(url, {
+ signal,
+ headers: { Accept: 'application/json' },
+ })
+
+ const text = await response.text()
+ let body: unknown = {}
+
+ try {
+ body = text ? JSON.parse(text) : {}
+ } catch {
+ body = text
+ }
+
+ if (!response.ok) {
+ const err: HttpError = new Error(
+ typeof body === 'object' && body && 'error' in body
+ ? String((body as { error?: string }).error || response.statusText)
+ : response.statusText
+ )
+ err.status = response.status
+ err.body = body
+ throw err
+ }
+
+ return body as AboutSnapshotResponse
+}
+
+export const useAboutSnapshotStats = () => {
+ const { data, error, isLoading, isValidating } = useSWR<
+ AboutSnapshotResponse,
+ HttpError
+ >(['/api/about/snapshot'] as const, fetcher, {
+ revalidateOnFocus: false,
+ revalidateOnReconnect: true,
+ dedupingInterval: 60000,
+ })
+
+ return {
+ data,
+ error,
+ isLoading: isLoading || isValidating,
+ }
+}
diff --git a/apps/web/src/modules/about/utils.ts b/apps/web/src/modules/about/utils.ts
new file mode 100644
index 000000000..68f890261
--- /dev/null
+++ b/apps/web/src/modules/about/utils.ts
@@ -0,0 +1,34 @@
+import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains'
+import removeMd from 'remove-markdown'
+
+// Normalize chain labels for fuzzy matching/deduplication:
+// lowercase, replace non-alphanumerics with spaces, then collapse/trim whitespace.
+const normalizeChainLabel = (value?: string | null) =>
+ (value || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, ' ')
+ .trim()
+
+const CHAIN_ICON_BY_LABEL = new Map(
+ PUBLIC_DEFAULT_CHAINS.flatMap((chain) => {
+ const labels = [chain.name, chain.slug].filter(Boolean).map(normalizeChainLabel)
+ const fallbackLabels = labels.flatMap((label) => {
+ if (!label.includes('sepolia')) return []
+
+ return [label.replace(/\s*sepolia$/, '').trim()]
+ })
+
+ return [...labels, ...fallbackLabels]
+ .filter(Boolean)
+ .map((label) => [label, chain.icon] as const)
+ })
+)
+
+export const getChainLogoSrc = (chainName?: string | null) =>
+ CHAIN_ICON_BY_LABEL.get(normalizeChainLabel(chainName)) || null
+
+export const toPlainText = (value?: string | null) =>
+ removeMd(value || '')
+ .replace(/\s+/g, ' ')
+ .trim()
diff --git a/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts b/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts
index c5171a9e3..b9f98c63f 100644
--- a/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts
+++ b/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts
@@ -4,7 +4,11 @@ import { style } from '@vanilla-extract/css'
export const commentFormContainer = style([
atoms({
w: '100%',
- backgroundColor: 'background2',
+ p: 'x4',
+ backgroundColor: 'background1',
+ borderColor: 'border',
+ borderStyle: 'solid',
+ borderWidth: 'thin',
borderRadius: 'curved',
mb: 'x5',
}),
diff --git a/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts b/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts
index 33dd250c7..a55c94cff 100644
--- a/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts
+++ b/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts
@@ -18,7 +18,8 @@ export const commentCard = style([
borderRadius: 'curved',
borderWidth: 'thin',
borderStyle: 'solid',
- borderColor: 'borderOnImage',
+ borderColor: 'border',
+ backgroundColor: 'background1',
py: 'x2',
px: 'x4',
}),
diff --git a/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx b/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx
index 5238464ae..ce802c76b 100644
--- a/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx
+++ b/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx
@@ -56,7 +56,7 @@ const CommentCard: React.FC<{
nameStyle={{ fontSize: '16px' }}
mobileTapBehavior="toggle"
/>
-
+
{timeAgo}
diff --git a/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts b/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts
index d3524b51c..240f799e5 100644
--- a/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts
+++ b/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts
@@ -41,6 +41,9 @@ export const coinInfoPanel = style([
p: 'x6',
borderRadius: 'curved',
backgroundColor: 'background2',
+ borderColor: 'border',
+ borderWidth: 'thin',
+ borderStyle: 'solid',
}),
])
@@ -49,6 +52,9 @@ export const swapPanel = style([
p: 'x6',
borderRadius: 'curved',
backgroundColor: 'background2',
+ borderColor: 'border',
+ borderWidth: 'thin',
+ borderStyle: 'solid',
}),
{
'@media': {
diff --git a/apps/web/src/modules/dashboard/CreateActions.css.ts b/apps/web/src/modules/dashboard/CreateActions.css.ts
index e42d1142f..35328a602 100644
--- a/apps/web/src/modules/dashboard/CreateActions.css.ts
+++ b/apps/web/src/modules/dashboard/CreateActions.css.ts
@@ -1,8 +1,5 @@
-import { vars } from '@buildeross/zord'
import { style } from '@vanilla-extract/css'
-const darkButtonHover = vars.color.neutralHover
-
export const actionButtons = style({
width: '100%',
'@media': {
@@ -11,16 +8,3 @@ export const actionButtons = style({
},
},
})
-
-export const daoButton = style({
- borderColor: `${vars.color.border} !important`,
-})
-
-export const createPostButton = style({
- selectors: {
- 'html[data-theme-mode="dark"] &:hover': {
- backgroundColor: `${darkButtonHover} !important`,
- borderColor: `${darkButtonHover} !important`,
- },
- },
-})
diff --git a/apps/web/src/modules/dashboard/CreateActions.tsx b/apps/web/src/modules/dashboard/CreateActions.tsx
index 6bbcd7e6c..cf59ab9d4 100644
--- a/apps/web/src/modules/dashboard/CreateActions.tsx
+++ b/apps/web/src/modules/dashboard/CreateActions.tsx
@@ -3,7 +3,7 @@ import { Button, Flex } from '@buildeross/zord'
import Link from 'next/link'
import React, { useState } from 'react'
-import { actionButtons, createPostButton, daoButton } from './CreateActions.css'
+import { actionButtons } from './CreateActions.css'
import { DaoSelectorModal } from './DaoSelectorModal'
export interface CreateActionsProps {
@@ -28,23 +28,21 @@ export const CreateActions: React.FC = ({ userAddress }) =>
<>
-
-
-
- Create a DAO
-
-
+
+ Create a DAO
+
{
dao.proposals.some(
(proposal) =>
proposal.state === ProposalState.Active &&
- !proposal.votes.some((vote) => vote.voter === address.toLowerCase())
+ !proposal.votes.some(
+ (vote: { voter: string }) => vote.voter === address.toLowerCase()
+ )
)
)
}, [sortedDaos, address])
diff --git a/apps/web/src/modules/dashboard/SingleDaoSelector.tsx b/apps/web/src/modules/dashboard/SingleDaoSelector.tsx
index 20ac9a96a..2ce2bd70e 100644
--- a/apps/web/src/modules/dashboard/SingleDaoSelector.tsx
+++ b/apps/web/src/modules/dashboard/SingleDaoSelector.tsx
@@ -1,6 +1,7 @@
import { COIN_SUPPORTED_CHAIN_IDS, PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants'
import { SearchInput } from '@buildeross/feed-ui'
import { useDaoSearch, useDaosWithClankerTokens, useUserDaos } from '@buildeross/hooks'
+import type { MyDaosResponse } from '@buildeross/sdk/subgraph'
import type {
AddressType,
CHAIN_ID,
@@ -40,6 +41,7 @@ export interface SingleDaoSelectorProps {
}
const MIN_SEARCH_LENGTH = 3
+type MemberDao = MyDaosResponse[number]
export const SingleDaoSelector: React.FC = ({
chainIds,
@@ -60,7 +62,7 @@ export const SingleDaoSelector: React.FC = ({
// Convert MyDaosResponse to DaoListItem format (before filtering)
const allDaoItems: DaoListItem[] = useMemo(() => {
- let items = memberDaos.map((dao) => ({
+ let items: DaoListItem[] = memberDaos.map((dao: MemberDao) => ({
name: dao.name,
image: dao.contractImage,
address: dao.collectionAddress.toLowerCase() as AddressType,
@@ -84,13 +86,13 @@ export const SingleDaoSelector: React.FC = ({
// Group DAOs by chain for checking clanker tokens
const daosByChain = useMemo(() => {
- const grouped: Record = {} as Record
+ const grouped: Partial> = {}
allDaoItems.forEach((dao) => {
if (isChainIdSupportedByCoining(dao.chainId)) {
if (!grouped[dao.chainId]) {
grouped[dao.chainId] = []
}
- grouped[dao.chainId].push(dao)
+ grouped[dao.chainId]!.push(dao)
}
})
return grouped
@@ -98,11 +100,11 @@ export const SingleDaoSelector: React.FC = ({
// Fetch clanker tokens for each chain
const chainQueries = COIN_SUPPORTED_CHAIN_IDS.map((chainId) => {
- const daosOnChain = daosByChain[chainId] || []
+ const daosOnChain: DaoListItem[] = daosByChain[chainId as CHAIN_ID] || []
// eslint-disable-next-line react-hooks/rules-of-hooks
return useDaosWithClankerTokens({
chainId,
- daoAddresses: daosOnChain.map((dao) => dao.address),
+ daoAddresses: daosOnChain.map((dao: DaoListItem) => dao.address),
enabled: actionType === 'post' && daosOnChain.length > 0,
})
})
diff --git a/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts b/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts
index a2251b0a6..309afcc24 100644
--- a/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts
+++ b/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts
@@ -41,12 +41,20 @@ export const dropInfoPanel = style([
p: 'x6',
borderRadius: 'curved',
backgroundColor: 'background2',
+ borderColor: 'border',
+ borderWidth: 'thin',
+ borderStyle: 'solid',
}),
])
export const mintPanel = style([
atoms({
+ p: 'x6',
borderRadius: 'curved',
+ backgroundColor: 'background2',
+ borderColor: 'border',
+ borderWidth: 'thin',
+ borderStyle: 'solid',
}),
{
'@media': {
diff --git a/apps/web/src/modules/drop/DropDetail/DropDetail.tsx b/apps/web/src/modules/drop/DropDetail/DropDetail.tsx
index 0d584d6bd..6f2638890 100644
--- a/apps/web/src/modules/drop/DropDetail/DropDetail.tsx
+++ b/apps/web/src/modules/drop/DropDetail/DropDetail.tsx
@@ -122,6 +122,7 @@ export const DropDetail = ({
saleEnd={saleEnd}
editionSize={drop.editionSize}
maxPerAddress={parseInt(drop.maxSalePurchasePerAddress)}
+ unstyledContainer
/>
@@ -167,6 +168,7 @@ export const DropDetail = ({
saleEnd={saleEnd}
editionSize={drop.editionSize}
maxPerAddress={parseInt(drop.maxSalePurchasePerAddress)}
+ unstyledContainer
/>
diff --git a/apps/web/src/pages/about.tsx b/apps/web/src/pages/about.tsx
index 773bf8085..bc2f09451 100644
--- a/apps/web/src/pages/about.tsx
+++ b/apps/web/src/pages/about.tsx
@@ -1,98 +1,16 @@
-import { useChainStore } from '@buildeross/stores'
-import { ContractButton } from '@buildeross/ui/ContractButton'
-import { Box, Stack, vars } from '@buildeross/zord'
import Head from 'next/head'
-import Image from 'next/image'
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import React, { useCallback } from 'react'
import { getDefaultLayout } from 'src/layouts/DefaultLayout'
-import { darkWhy, lightWhy, whyCreateButton, whyTextStyle } from 'src/styles/about.css'
+import { AboutPageView } from 'src/modules/about'
import { NextPageWithLayout } from './_app'
const AboutPage: NextPageWithLayout = () => {
- const { push } = useRouter()
- const { id: chainId } = useChainStore((x) => x.chain)
-
- const handleCreateClick = useCallback(() => {
- push('/create')
- }, [push])
-
return (
<>
Nouns Builder | About
-
-
-
-
-
-
-
- Nouns Builder makes it easy for communities and collectives to create Nounish
- DAOs, fully equipped with onchain governance and membership auctions starting
- day one.
-
-
- This public good DAO tooling and the Nouns Builder Protocol are maintained and
- governed by
-
- BuilderDAO
-
- . Learn more about the DAO's vision and mission
-
- here
-
- .
-
-
-
- Create a DAO
-
-
-
+
>
)
}
diff --git a/apps/web/src/pages/api/about/dao-tabs.ts b/apps/web/src/pages/api/about/dao-tabs.ts
new file mode 100644
index 000000000..b9834c1db
--- /dev/null
+++ b/apps/web/src/pages/api/about/dao-tabs.ts
@@ -0,0 +1,613 @@
+import { CACHE_TIMES } from '@buildeross/constants/cacheTimes'
+import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains'
+import { PUBLIC_SUBGRAPH_URL } from '@buildeross/constants/subgraph'
+import { Auction_OrderBy, exploreDaosRequest } from '@buildeross/sdk/subgraph'
+import { CHAIN_ID } from '@buildeross/types'
+import { chainIdToSlug } from '@buildeross/utils/chains'
+import { gql, GraphQLClient } from 'graphql-request'
+import type { NextApiRequest, NextApiResponse } from 'next'
+import { summarizeDaoDescription } from 'src/utils/api/ai/summaries'
+import { withCors } from 'src/utils/api/cors'
+import { withRateLimit } from 'src/utils/api/rateLimit'
+import { parseEther } from 'viem'
+
+import type { AboutDao, AboutDaoTabsResponse } from '../../../modules/about/types'
+
+const MIN_AUCTION_SALES = parseEther('0.001').toString()
+const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30
+
+type DaoQueryItem = {
+ id: string
+ name: string
+ description: string
+ tokenAddress: string
+ contractImage?: string | null
+ totalSupply: number
+ totalAuctionSales: string
+ latestToken: Array<{ name: string; image?: string | null; tokenId: string }>
+ firstToken: Array<{ mintedAt: string }>
+ recentAuctions: Array<{
+ id: string
+ endTime: string
+ winningBid?: { amount: string } | null
+ }>
+ currentAuction?: {
+ endTime: string
+ highestBid?: { amount: string } | null
+ token?: { tokenId: string; name: string; image?: string | null } | null
+ } | null
+}
+
+type DaoQueryResponse = {
+ daos: DaoQueryItem[]
+}
+
+type CrossChainDaoCandidate = {
+ chainId: CHAIN_ID
+ dao: DaoQueryItem
+}
+
+type CrossChainActiveDao = {
+ chainId: CHAIN_ID
+ dao: NonNullable>>['daos'][number]
+}
+
+type SettledChainResult = {
+ chainId: CHAIN_ID
+ value: T
+}
+
+const getFulfilledChainResults = (
+ settledResults: PromiseSettledResult>[],
+ context: string
+) =>
+ settledResults.flatMap((result) => {
+ if (result.status === 'fulfilled') {
+ return [result.value.value]
+ }
+
+ const reason = result.reason as { chainId?: CHAIN_ID; error?: unknown }
+ console.error(
+ `About dao tabs ${context} failed for chain ${String(reason?.chainId ?? 'unknown')}:`,
+ reason?.error || reason
+ )
+ return []
+ })
+
+type FeaturedDaoConfigItem = {
+ name: string
+ aliases?: readonly string[]
+ description: string
+ signalLabel: string
+ signalValue: string
+ fallbackChainId: CHAIN_ID
+}
+
+const ABOUT_DAO_TABS_QUERY = gql`
+ query AboutDaoTabs(
+ $daoFirst: Int!
+ $daoWhere: DAO_filter
+ $recentAuctionWhere: Auction_filter
+ $recentAuctionFirst: Int!
+ ) {
+ daos(
+ first: $daoFirst
+ where: $daoWhere
+ orderBy: totalAuctionSales
+ orderDirection: desc
+ ) {
+ id
+ name
+ description
+ tokenAddress
+ contractImage
+ totalSupply
+ totalAuctionSales
+ latestToken: tokens(first: 1, orderBy: mintedAt, orderDirection: desc) {
+ name
+ image
+ tokenId
+ }
+ firstToken: tokens(first: 1, orderBy: mintedAt, orderDirection: asc) {
+ mintedAt
+ }
+ currentAuction {
+ endTime
+ highestBid {
+ amount
+ }
+ token {
+ tokenId
+ name
+ image
+ }
+ }
+ recentAuctions: auctions(
+ first: $recentAuctionFirst
+ orderBy: endTime
+ orderDirection: desc
+ where: $recentAuctionWhere
+ ) {
+ id
+ endTime
+ winningBid {
+ amount
+ }
+ }
+ }
+ }
+`
+
+const buildDaoHref = (chainId: CHAIN_ID, tokenAddress: string) =>
+ `/dao/${chainIdToSlug(chainId)}/${tokenAddress}`
+
+const getInitials = (name: string) =>
+ name
+ .split(' ')
+ .slice(0, 2)
+ .map((part) => part[0]?.toUpperCase() ?? '')
+ .join('')
+
+const FEATURED_DAO_CONFIG: readonly FeaturedDaoConfigItem[] = [
+ {
+ name: 'Builder DAO',
+ aliases: ['BuilderDAO'],
+ description: 'Stewards Builder upgrades, ecosystem tooling, and protocol operations.',
+ signalLabel: 'Role',
+ signalValue: 'Protocol steward',
+ fallbackChainId: CHAIN_ID.BASE,
+ },
+ {
+ name: 'Gnars DAO',
+ aliases: ['Gnars'],
+ description: 'Funds skate, surf, and snow culture through auctions and governance.',
+ signalLabel: 'Focus',
+ signalValue: 'Sports culture',
+ fallbackChainId: CHAIN_ID.BASE,
+ },
+ {
+ name: 'Collective Nouns',
+ description: 'Explores shared ownership and member-led treasury deployment.',
+ signalLabel: 'Focus',
+ signalValue: 'Art collective',
+ fallbackChainId: CHAIN_ID.BASE,
+ },
+ {
+ name: 'Nouns DAO Africa',
+ description: 'Supports regional builders and creatives using Nounish governance.',
+ signalLabel: 'Focus',
+ signalValue: 'Regional empowerment',
+ fallbackChainId: CHAIN_ID.ETHEREUM,
+ },
+] as const
+
+const chainById = new Map(
+ PUBLIC_DEFAULT_CHAINS.map((chain) => [chain.id, chain] as const)
+)
+
+const normalizeDaoName = (name: string) =>
+ name
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]/g, '')
+
+const isNameMatch = (targets: string[], candidateName?: string | null) => {
+ const candidate = normalizeDaoName(candidateName || '')
+
+ if (!candidate) return false
+
+ return targets.some((target) => {
+ const normalizedTarget = normalizeDaoName(target)
+
+ return (
+ candidate === normalizedTarget ||
+ candidate.includes(normalizedTarget) ||
+ normalizedTarget.includes(candidate)
+ )
+ })
+}
+
+const shortenDescription = (description?: string | null) => {
+ const base = description?.trim()
+
+ if (!base) return null
+
+ const [firstSentence] = base.split(/(?<=[.!?])\s+/)
+ const concise = firstSentence || base
+
+ return concise.length > 110 ? `${concise.slice(0, 107).trimEnd()}...` : concise
+}
+
+const createDaoDescriptionResolver = () => {
+ const summaries = new Map>()
+ const aiConfigured = Boolean(
+ process.env.AI_GATEWAY_API_KEY || process.env.OPENAI_API_KEY
+ )
+
+ return (description?: string | null) => {
+ const base = description?.trim()
+
+ if (!base) {
+ return Promise.resolve(null)
+ }
+
+ if (!aiConfigured) {
+ return Promise.resolve(shortenDescription(base))
+ }
+
+ if (!summaries.has(base)) {
+ summaries.set(
+ base,
+ summarizeDaoDescription(base)
+ .then((summary) => summary || shortenDescription(base))
+ .catch(() => shortenDescription(base))
+ )
+ }
+
+ return summaries.get(base)!
+ }
+}
+
+const mapDao = (
+ dao: DaoQueryItem,
+ chainId: CHAIN_ID,
+ _index: number,
+ signalLabel: string,
+ signalValue: string,
+ badge: string,
+ category: AboutDao['category'],
+ descriptionOverride?: string | null
+): AboutDao => ({
+ id: `${dao.id}-${badge.toLowerCase()}`,
+ name: dao.name || 'Untitled DAO',
+ description:
+ descriptionOverride ||
+ shortenDescription(dao.description) ||
+ 'Live Builder DAO with recent auction and member activity.',
+ signalLabel,
+ signalValue,
+ href: buildDaoHref(chainId, dao.tokenAddress),
+ badge,
+ initials: getInitials(dao.name || 'DAO'),
+ imageUrl: dao.contractImage || dao.latestToken[0]?.image || null,
+ recentAuctionImage:
+ dao.currentAuction?.token?.image || dao.latestToken[0]?.image || null,
+ chainIcon: chainById.get(chainId)?.icon || null,
+ chainName: chainById.get(chainId)?.name || null,
+ category,
+})
+
+const fetchDaoCandidates = async (
+ chainId: CHAIN_ID,
+ options: { totalAuctionSalesGt?: string; first?: number; nameContains?: string } = {}
+): Promise => {
+ const subgraphUrl = PUBLIC_SUBGRAPH_URL.get(chainId)
+
+ if (!subgraphUrl) {
+ throw new Error(`No subgraph URL configured for chain ${chainId}`)
+ }
+
+ const client = new GraphQLClient(subgraphUrl, {
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ const now = Math.floor(Date.now() / 1000)
+
+ const data = await client.request(ABOUT_DAO_TABS_QUERY, {
+ daoFirst: options.first ?? 150,
+ daoWhere: {
+ totalSupply_gt: 0,
+ ...(options.nameContains ? { name_contains_nocase: options.nameContains } : {}),
+ ...(options.totalAuctionSalesGt
+ ? { totalAuctionSales_gt: options.totalAuctionSalesGt }
+ : {}),
+ },
+ recentAuctionFirst: 40,
+ recentAuctionWhere: {
+ settled: true,
+ endTime_gt: now - THIRTY_DAYS_IN_SECONDS,
+ },
+ })
+
+ return data.daos ?? []
+}
+
+const fetchCrossChainDaoCandidates = async (
+ options: { totalAuctionSalesGt?: string; first?: number } = {}
+): Promise => {
+ const settledResults = await Promise.allSettled(
+ PUBLIC_DEFAULT_CHAINS.map(async (chain) => {
+ try {
+ const daos = await fetchDaoCandidates(chain.id, options)
+
+ return {
+ chainId: chain.id,
+ value: daos.map((dao) => ({ chainId: chain.id, dao })),
+ }
+ } catch (error) {
+ throw { chainId: chain.id, error }
+ }
+ })
+ )
+
+ return getFulfilledChainResults(settledResults, 'candidate fetch').flat()
+}
+
+const findFeaturedMatch = (targets: string[], candidates: CrossChainDaoCandidate[]) =>
+ candidates.find((item) => isNameMatch(targets, item.dao.name))
+
+const fetchFeaturedMatch = async (targets: string[]) => {
+ const settledResults = await Promise.allSettled(
+ PUBLIC_DEFAULT_CHAINS.flatMap((chain) =>
+ targets.map(async (target) => {
+ try {
+ const daos = await fetchDaoCandidates(chain.id, {
+ first: 25,
+ nameContains: target,
+ })
+
+ return {
+ chainId: chain.id,
+ value: daos.map((dao) => ({ chainId: chain.id, dao })),
+ }
+ } catch (error) {
+ throw { chainId: chain.id, error }
+ }
+ })
+ )
+ )
+
+ return findFeaturedMatch(
+ targets,
+ getFulfilledChainResults(settledResults, 'featured match fetch').flat()
+ )
+}
+
+const buildFeaturedDaos = async (
+ resolveDescription: ReturnType
+): Promise => {
+ const settledResults = await Promise.allSettled(
+ PUBLIC_DEFAULT_CHAINS.map(async (chain) => {
+ try {
+ const daos = await fetchDaoCandidates(chain.id, { first: 800 })
+
+ return {
+ chainId: chain.id,
+ value: daos.map((dao) => ({ chainId: chain.id, dao })),
+ }
+ } catch (error) {
+ throw { chainId: chain.id, error }
+ }
+ })
+ )
+
+ const featuredCandidates = getFulfilledChainResults(
+ settledResults,
+ 'featured candidate fetch'
+ ).flat()
+
+ return Promise.all(
+ FEATURED_DAO_CONFIG.map(async (item, index) => {
+ const targets = [item.name, ...(item.aliases ?? [])]
+ const match =
+ findFeaturedMatch(targets, featuredCandidates) ||
+ (await fetchFeaturedMatch(targets))
+
+ if (!match) {
+ return {
+ id: `${normalizeDaoName(item.name).replace(/\s+/g, '-')}-featured`,
+ name: item.name,
+ description: item.description,
+ signalLabel: item.signalLabel,
+ signalValue: item.signalValue,
+ href: '/explore',
+ badge: 'Featured',
+ initials: getInitials(item.name),
+ imageUrl: null,
+ recentAuctionImage: null,
+ chainIcon: chainById.get(item.fallbackChainId)?.icon || null,
+ chainName: chainById.get(item.fallbackChainId)?.name || null,
+ category: 'featured' as const,
+ }
+ }
+
+ const description =
+ (await resolveDescription(match.dao.description || item.description)) ||
+ shortenDescription(match.dao.description || item.description)
+
+ return {
+ ...mapDao(
+ {
+ ...match.dao,
+ description: match.dao.description || item.description,
+ },
+ match.chainId,
+ index,
+ item.signalLabel,
+ item.signalValue,
+ 'Featured',
+ 'featured',
+ description
+ ),
+ name: item.name,
+ }
+ })
+ )
+}
+
+const buildTrendingDaos = async (
+ items: CrossChainDaoCandidate[],
+ resolveDescription: ReturnType
+) =>
+ Promise.all(
+ items
+ .map((item) => ({
+ ...item,
+ recentSalesCount: item.dao.recentAuctions.length,
+ recentVolume: item.dao.recentAuctions.reduce(
+ (total, auction) => total + BigInt(auction.winningBid?.amount ?? '0'),
+ 0n
+ ),
+ }))
+ .filter((item) => item.recentSalesCount > 0)
+ .sort((a, b) => {
+ if (b.recentSalesCount !== a.recentSalesCount) {
+ return b.recentSalesCount - a.recentSalesCount
+ }
+ if (b.recentVolume === a.recentVolume) {
+ return 0
+ }
+ return b.recentVolume > a.recentVolume ? 1 : -1
+ })
+ .slice(0, 4)
+ .map(async (item, index) =>
+ mapDao(
+ item.dao,
+ item.chainId,
+ index,
+ 'Sales in 30d',
+ `${item.recentSalesCount} settled`,
+ 'Trending',
+ 'trending',
+ (await resolveDescription(item.dao.description)) ||
+ shortenDescription(item.dao.description)
+ )
+ )
+ )
+
+const buildNewDaos = async (
+ items: CrossChainDaoCandidate[],
+ resolveDescription: ReturnType
+) =>
+ Promise.all(
+ items
+ .filter((item) => item.dao.totalSupply > 3 && item.dao.firstToken[0]?.mintedAt)
+ .sort(
+ (a, b) =>
+ Number(b.dao.firstToken[0].mintedAt) - Number(a.dao.firstToken[0].mintedAt)
+ )
+ .slice(0, 4)
+ .map(async (item, index) =>
+ mapDao(
+ item.dao,
+ item.chainId,
+ index,
+ 'Supply sold',
+ `${item.dao.totalSupply} tokens`,
+ 'New',
+ 'new',
+ (await resolveDescription(item.dao.description)) ||
+ shortenDescription(item.dao.description)
+ )
+ )
+ )
+
+const buildActiveDaos = async (
+ daoCandidates: CrossChainDaoCandidate[],
+ resolveDescription: ReturnType
+) => {
+ const now = Math.floor(Date.now() / 1000)
+ const settledResults = await Promise.allSettled(
+ PUBLIC_DEFAULT_CHAINS.map(async (chain) => {
+ try {
+ const result = await exploreDaosRequest(chain.id, 12, 0, Auction_OrderBy.EndTime)
+
+ return {
+ chainId: chain.id,
+ value: (result?.daos ?? []).map((dao) => ({
+ chainId: chain.id,
+ dao,
+ })),
+ }
+ } catch (error) {
+ throw { chainId: chain.id, error }
+ }
+ })
+ )
+
+ const activeDaos = getFulfilledChainResults(
+ settledResults,
+ 'active dao fetch'
+ ).flat() as CrossChainActiveDao[]
+
+ return Promise.all(
+ activeDaos
+ .filter((item) => item.dao.endTime && Number(item.dao.endTime) > now)
+ .sort((a, b) => Number(a.dao.endTime) - Number(b.dao.endTime))
+ .slice(0, 4)
+ .map(async (item) => {
+ const matchingDao = daoCandidates.find(
+ (candidate) =>
+ candidate.chainId === item.chainId &&
+ candidate.dao.tokenAddress.toLowerCase() ===
+ item.dao.dao.tokenAddress.toLowerCase()
+ )
+
+ const descriptionSource = matchingDao?.dao.description
+ const description =
+ (await resolveDescription(descriptionSource)) ||
+ shortenDescription(descriptionSource) ||
+ 'Live Builder DAO with recent auction and member activity.'
+
+ return {
+ id: `${item.chainId}-${item.dao.dao.tokenAddress}-active`,
+ name: item.dao.dao.name || item.dao.token?.name || 'Untitled DAO',
+ description,
+ signalLabel: 'Ends soon',
+ signalValue: `${Math.max(1, Math.round((Number(item.dao.endTime) - now) / 3600))}h left`,
+ href: buildDaoHref(item.chainId, item.dao.dao.tokenAddress),
+ badge: 'Active',
+ initials: getInitials(item.dao.dao.name || item.dao.token?.name || 'DAO'),
+ imageUrl: matchingDao?.dao.contractImage || item.dao.token?.image || null,
+ recentAuctionImage: item.dao.token?.image || null,
+ chainIcon: chainById.get(item.chainId)?.icon || null,
+ chainName: chainById.get(item.chainId)?.name || null,
+ category: 'active' as const,
+ }
+ })
+ )
+}
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ if (req.method !== 'GET') {
+ return res.status(405).json({ error: 'Method not allowed' })
+ }
+
+ try {
+ const resolveDescription = createDaoDescriptionResolver()
+
+ const [featuredDaos, daoCandidates] = await Promise.all([
+ buildFeaturedDaos(resolveDescription),
+ fetchCrossChainDaoCandidates({
+ totalAuctionSalesGt: MIN_AUCTION_SALES,
+ first: 150,
+ }),
+ ])
+
+ const payload: AboutDaoTabsResponse = {
+ featured: featuredDaos,
+ trending: await buildTrendingDaos(daoCandidates, resolveDescription),
+ new: await buildNewDaos(daoCandidates, resolveDescription),
+ active: await buildActiveDaos(daoCandidates, resolveDescription),
+ }
+
+ const { maxAge, swr } = CACHE_TIMES.EXPLORE
+ res.setHeader(
+ 'Cache-Control',
+ `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}`
+ )
+
+ return res.status(200).json(payload)
+ } catch (error) {
+ console.error('About dao tabs error:', error)
+ return res.status(500).json({ error: 'Failed to load about page DAO tabs' })
+ }
+}
+
+export default withCors()(
+ withRateLimit({
+ maxRequests: 60,
+ windowSeconds: 60,
+ keyPrefix: 'about:dao-tabs',
+ })(handler)
+)
diff --git a/apps/web/src/pages/api/about/showcase.ts b/apps/web/src/pages/api/about/showcase.ts
new file mode 100644
index 000000000..a67f5d49a
--- /dev/null
+++ b/apps/web/src/pages/api/about/showcase.ts
@@ -0,0 +1,416 @@
+import { CACHE_TIMES } from '@buildeross/constants/cacheTimes'
+import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains'
+import { PUBLIC_SUBGRAPH_URL } from '@buildeross/constants/subgraph'
+import { CHAIN_ID } from '@buildeross/types'
+import { DEFAULT_ZORA_TOTAL_SUPPLY, walletSnippet } from '@buildeross/utils'
+import { chainIdToSlug } from '@buildeross/utils/chains'
+import { gql, GraphQLClient } from 'graphql-request'
+import type { NextApiRequest, NextApiResponse } from 'next'
+import { getZoraCoinUsdPrice } from 'src/services/coinPriceService'
+import { withCors } from 'src/utils/api/cors'
+import { withRateLimit } from 'src/utils/api/rateLimit'
+import { formatEther } from 'viem'
+
+import {
+ coiningHighlights as fallbackCoiningHighlights,
+ dropHighlights as fallbackDropHighlights,
+ proposalHighlights as fallbackProposalHighlights,
+} from '../../../modules/about/data'
+import type {
+ AboutShowcaseResponse,
+ CoiningHighlight,
+ DroposalHighlight,
+ DroposalStatus,
+} from '../../../modules/about/types'
+
+type ShowcaseZoraDrop = {
+ id: string
+ name: string
+ description?: string | null
+ imageURI?: string | null
+ animationURI?: string | null
+ creator: string
+ totalSalesAmount: string
+ createdAt: string
+ dao?: {
+ name: string
+ tokenAddress: string
+ } | null
+}
+
+type ShowcaseZoraCoin = {
+ id: string
+ coinAddress: string
+ name: string
+ symbol: string
+ caller: string
+ createdAt: string
+ dao?: {
+ name: string
+ tokenAddress: string
+ } | null
+}
+
+type ShowcaseProposal = {
+ id: string
+ proposalId: string
+ proposalNumber: number
+ title?: string | null
+ description?: string | null
+ timeCreated: string
+ voteEnd: string
+ voteCount: number
+ forVotes: number
+ againstVotes: number
+ quorumVotes: string
+ queued: boolean
+ executed: boolean
+ canceled: boolean
+ vetoed: boolean
+ dao: {
+ name: string
+ tokenAddress: string
+ }
+}
+
+type ShowcaseQueryResponse = {
+ zoraCoins: ShowcaseZoraCoin[]
+ zoraDrops: ShowcaseZoraDrop[]
+ proposals: ShowcaseProposal[]
+}
+
+type CrossChainCoin = ShowcaseZoraCoin & { chainId: CHAIN_ID }
+type CrossChainDrop = ShowcaseZoraDrop & { chainId: CHAIN_ID }
+type CrossChainProposal = ShowcaseProposal & { chainId: CHAIN_ID }
+
+const ABOUT_SHOWCASE_QUERY = gql`
+ query AboutShowcase($coinFirst: Int!, $dropFirst: Int!, $proposalFirst: Int!) {
+ zoraCoins(
+ first: $coinFirst
+ orderBy: createdAt
+ orderDirection: desc
+ where: { dao_not: null }
+ ) {
+ id
+ coinAddress
+ name
+ symbol
+ caller
+ createdAt
+ dao {
+ name
+ tokenAddress
+ }
+ }
+ zoraDrops(
+ first: $dropFirst
+ orderBy: totalSalesAmount
+ orderDirection: desc
+ where: { dao_not: null, totalSalesAmount_gt: 0 }
+ ) {
+ id
+ name
+ description
+ imageURI
+ animationURI
+ creator
+ totalSalesAmount
+ createdAt
+ dao {
+ name
+ tokenAddress
+ }
+ }
+ proposals(
+ first: $proposalFirst
+ orderBy: timeCreated
+ orderDirection: desc
+ where: { title_not: null }
+ ) {
+ id
+ proposalId
+ proposalNumber
+ title
+ description
+ timeCreated
+ voteEnd
+ voteCount
+ forVotes
+ againstVotes
+ quorumVotes
+ queued
+ executed
+ canceled
+ vetoed
+ dao {
+ name
+ tokenAddress
+ }
+ }
+ }
+`
+
+const compactVotes = (votes: number) =>
+ `${new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(votes)} voters`
+
+const compactMarketCap = (marketCapUsd: number) =>
+ `$${new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(marketCapUsd)} market cap`
+
+const compactEthAmount = (amountInEth: number) =>
+ `ETH ${new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(amountInEth)} sold`
+
+const previewSurfaces = [
+ 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)',
+ 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)',
+ 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)',
+ 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)',
+]
+
+const cleanSentence = (value?: string | null) => {
+ const base = value
+ ?.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
+ ?.replace(/\[[^\]]+\]\([^)]+\)/g, ' ')
+ ?.replace(/[`*_>#-]/g, ' ')
+ ?.replace(/\s+/g, ' ')
+ ?.trim()
+
+ if (!base) return null
+
+ const [firstSentence] = base.split(/(?<=[.!?])\s+/)
+ const concise = firstSentence || base
+
+ return concise.length > 120 ? `${concise.slice(0, 117).trimEnd()}...` : concise
+}
+
+const deriveProposalStatus = (
+ proposal: ShowcaseProposal,
+ now: number
+): DroposalStatus | null => {
+ if (proposal.canceled || proposal.vetoed) return null
+ if (proposal.executed) return 'Executed'
+ if (proposal.queued) return 'Queued'
+ if (Number(proposal.voteEnd) > now) return 'Active'
+
+ const passed =
+ proposal.forVotes > proposal.againstVotes &&
+ BigInt(proposal.forVotes) >= BigInt(proposal.quorumVotes)
+
+ return passed ? 'Succeeded' : 'Defeated'
+}
+
+const fetchShowcaseDataForChain = async (
+ chainId: CHAIN_ID
+): Promise<{
+ coins: CrossChainCoin[]
+ drops: CrossChainDrop[]
+ proposals: CrossChainProposal[]
+}> => {
+ const subgraphUrl = PUBLIC_SUBGRAPH_URL.get(chainId)
+
+ if (!subgraphUrl) {
+ return { coins: [], drops: [], proposals: [] }
+ }
+
+ const client = new GraphQLClient(subgraphUrl, {
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ const data = await client.request(ABOUT_SHOWCASE_QUERY, {
+ coinFirst: 30,
+ dropFirst: 12,
+ proposalFirst: 20,
+ })
+
+ return {
+ coins: (data.zoraCoins ?? []).map((coin) => ({ ...coin, chainId })),
+ drops: (data.zoraDrops ?? []).map((drop) => ({ ...drop, chainId })),
+ proposals: (data.proposals ?? []).map((proposal) => ({ ...proposal, chainId })),
+ }
+}
+
+const buildCoiningHighlights = async (
+ coins: CrossChainCoin[]
+): Promise => {
+ const coinsWithMarketCap = await Promise.all(
+ coins
+ .filter((coin) => coin.dao?.tokenAddress && coin.coinAddress && coin.name)
+ .map(async (coin) => {
+ const priceUsd = await getZoraCoinUsdPrice(coin.coinAddress, coin.chainId).catch(
+ () => null
+ )
+
+ const marketCapUsd =
+ priceUsd && Number.isFinite(priceUsd)
+ ? priceUsd * DEFAULT_ZORA_TOTAL_SUPPLY
+ : null
+
+ return {
+ coin,
+ marketCapUsd,
+ }
+ })
+ )
+
+ return coinsWithMarketCap
+ .filter(
+ (
+ item
+ ): item is {
+ coin: CrossChainCoin
+ marketCapUsd: number
+ } => item.marketCapUsd !== null && item.marketCapUsd > 0
+ )
+ .sort((a, b) => b.marketCapUsd - a.marketCapUsd)
+ .slice(0, 4)
+ .map(({ coin, marketCapUsd }, index) => ({
+ id: `${coin.chainId}-${coin.id}`,
+ title: coin.name,
+ creator: walletSnippet(coin.caller as `0x${string}`),
+ dao: coin.dao?.name || 'Builder DAO',
+ chainLabel:
+ PUBLIC_DEFAULT_CHAINS.find((chain) => chain.id === coin.chainId)?.name || 'Base',
+ amount: compactMarketCap(marketCapUsd),
+ href: `/coin/${chainIdToSlug(coin.chainId)}/${coin.coinAddress}`,
+ eyebrow: 'DAO paired coin',
+ surface: previewSurfaces[index % previewSurfaces.length],
+ previewLabel: coin.symbol || 'Content coin',
+ }))
+}
+
+const buildDropHighlights = (drops: CrossChainDrop[]): DroposalHighlight[] => {
+ const now = Math.floor(Date.now() / 1000)
+
+ return drops
+ .filter((drop) => drop.dao?.tokenAddress && drop.name)
+ .sort((a, b) => {
+ const aAmount = BigInt(a.totalSalesAmount || '0')
+ const bAmount = BigInt(b.totalSalesAmount || '0')
+ return bAmount > aAmount ? 1 : bAmount < aAmount ? -1 : 0
+ })
+ .slice(0, 5)
+ .map((drop) => ({
+ id: `${drop.chainId}-${drop.id}`,
+ title: drop.name,
+ dao: drop.dao?.name || 'Builder DAO',
+ amount: compactEthAmount(Number(formatEther(BigInt(drop.totalSalesAmount || '0')))),
+ status: Number(drop.createdAt) > now - 60 * 60 * 24 * 14 ? 'Live' : 'Recent',
+ summary:
+ cleanSentence(drop.description) ||
+ 'A live onchain drop distributing content through Builder.',
+ href: `/drop/${chainIdToSlug(drop.chainId)}/${drop.id}`,
+ category:
+ PUBLIC_DEFAULT_CHAINS.find((chain) => chain.id === drop.chainId)?.name ||
+ 'Onchain drop',
+ }))
+}
+
+const buildProposalHighlights = (
+ proposals: CrossChainProposal[]
+): DroposalHighlight[] => {
+ const now = Math.floor(Date.now() / 1000)
+
+ return proposals
+ .map((proposal) => ({
+ proposal,
+ status: deriveProposalStatus(proposal, now),
+ }))
+ .filter(
+ (
+ item
+ ): item is {
+ proposal: CrossChainProposal
+ status: DroposalStatus
+ } => Boolean(item.status)
+ )
+ .sort((a, b) => {
+ if (a.status === 'Active' && b.status !== 'Active') return -1
+ if (a.status !== 'Active' && b.status === 'Active') return 1
+ return Number(b.proposal.timeCreated) - Number(a.proposal.timeCreated)
+ })
+ .slice(0, 5)
+ .map(({ proposal, status }) => ({
+ id: `${proposal.chainId}-${proposal.id}`,
+ title: proposal.title?.trim() || `Proposal #${proposal.proposalNumber}`,
+ dao: proposal.dao.name,
+ amount: compactVotes(proposal.voteCount || 0),
+ status,
+ summary:
+ cleanSentence(proposal.description) ||
+ `Proposal #${proposal.proposalNumber} in ${proposal.dao.name}.`,
+ href: `/dao/${chainIdToSlug(proposal.chainId)}/${proposal.dao.tokenAddress}/vote/${proposal.proposalId}`,
+ category:
+ PUBLIC_DEFAULT_CHAINS.find((chain) => chain.id === proposal.chainId)?.name ||
+ 'DAO proposal',
+ }))
+}
+
+const buildShowcaseResponse = async (): Promise => {
+ const chainData = await Promise.all(
+ PUBLIC_DEFAULT_CHAINS.map((chain) => fetchShowcaseDataForChain(chain.id))
+ )
+
+ const coins = chainData.flatMap((item) => item.coins)
+ const drops = chainData.flatMap((item) => item.drops)
+ const proposals = chainData.flatMap((item) => item.proposals)
+
+ return {
+ coining: await buildCoiningHighlights(coins),
+ drops: buildDropHighlights(drops),
+ proposals: buildProposalHighlights(proposals),
+ }
+}
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ if (req.method !== 'GET') {
+ return res.status(405).json({ error: 'Method not allowed' })
+ }
+
+ const { maxAge, swr } = CACHE_TIMES.EXPLORE
+
+ try {
+ const payload = await buildShowcaseResponse()
+
+ res.setHeader(
+ 'Cache-Control',
+ `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}`
+ )
+
+ return res.status(200).json({
+ coining: payload.coining.length ? payload.coining : fallbackCoiningHighlights,
+ drops: payload.drops.length ? payload.drops : fallbackDropHighlights,
+ proposals: payload.proposals.length
+ ? payload.proposals
+ : fallbackProposalHighlights,
+ })
+ } catch (error) {
+ console.error('About showcase error:', error)
+
+ res.setHeader(
+ 'Cache-Control',
+ `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}`
+ )
+
+ return res.status(200).json({
+ coining: fallbackCoiningHighlights,
+ drops: fallbackDropHighlights,
+ proposals: fallbackProposalHighlights,
+ })
+ }
+}
+
+export default withCors()(
+ withRateLimit({
+ maxRequests: 60,
+ windowSeconds: 60,
+ keyPrefix: 'about:showcase',
+ })(handler)
+)
diff --git a/apps/web/src/pages/api/about/snapshot.ts b/apps/web/src/pages/api/about/snapshot.ts
new file mode 100644
index 000000000..5a6822093
--- /dev/null
+++ b/apps/web/src/pages/api/about/snapshot.ts
@@ -0,0 +1,266 @@
+import { CACHE_TIMES } from '@buildeross/constants/cacheTimes'
+import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains'
+import { PUBLIC_SUBGRAPH_URL } from '@buildeross/constants/subgraph'
+import { gql, GraphQLClient } from 'graphql-request'
+import type { NextApiRequest, NextApiResponse } from 'next'
+import { withCors } from 'src/utils/api/cors'
+import { withRateLimit } from 'src/utils/api/rateLimit'
+import { formatEther } from 'viem'
+
+import type { AboutSnapshotResponse } from '../../../modules/about/types'
+
+type SnapshotDao = {
+ id: string
+ totalAuctionSales: string
+ proposalCount: number
+ ownerCount: number
+ totalSupply: number
+ currentAuction?: {
+ id: string
+ endTime: string
+ settled: boolean
+ } | null
+}
+
+type SnapshotQueryResponse = {
+ daos: SnapshotDao[]
+}
+
+type SnapshotOwner = {
+ owner: string
+}
+
+type SnapshotOwnersQueryResponse = {
+ daoTokenOwners: SnapshotOwner[]
+}
+
+const ABOUT_SNAPSHOT_QUERY = gql`
+ query AboutSnapshot($first: Int!, $where: DAO_filter) {
+ daos(first: $first, where: $where, orderBy: totalAuctionSales, orderDirection: desc) {
+ id
+ totalAuctionSales
+ proposalCount
+ ownerCount
+ totalSupply
+ currentAuction {
+ id
+ endTime
+ settled
+ }
+ }
+ }
+`
+
+const ABOUT_SNAPSHOT_OWNERS_QUERY = gql`
+ query AboutSnapshotOwners($first: Int!, $skip: Int!) {
+ daoTokenOwners(first: $first, skip: $skip) {
+ owner
+ }
+ }
+`
+
+const compactNumber = (value: number) =>
+ new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: value >= 1000 ? 1 : 0,
+ }).format(value)
+
+const compactEth = (value: number) =>
+ `ETH ${new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(value)}`
+
+const OWNER_PAGE_SIZE = 1000
+
+const fetchChainSnapshot = async (subgraphUrl: string): Promise => {
+ const client = new GraphQLClient(subgraphUrl, {
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ const data = await client.request(ABOUT_SNAPSHOT_QUERY, {
+ first: 1000,
+ where: {
+ totalSupply_gt: 0,
+ },
+ })
+
+ return data.daos ?? []
+}
+
+// TODO: This full scan of daoTokenOwners via paginated queries runs on every cache miss,
+// causing unbounded latency on the public API. Move this logic to a background job/cron that
+// periodically precomputes unique owner sets, then have the handler read that cached result.
+// Not done now because it requires a separate background process + persistence layer.
+const fetchUniqueOwnersForChain = async (subgraphUrl: string): Promise> => {
+ const client = new GraphQLClient(subgraphUrl, {
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ const owners = new Set()
+ let skip = 0
+
+ while (true) {
+ const data = await client.request(
+ ABOUT_SNAPSHOT_OWNERS_QUERY,
+ {
+ first: OWNER_PAGE_SIZE,
+ skip,
+ }
+ )
+
+ const page = data.daoTokenOwners ?? []
+
+ for (const item of page) {
+ if (item.owner) {
+ owners.add(item.owner.toLowerCase())
+ }
+ }
+
+ if (page.length < OWNER_PAGE_SIZE) {
+ break
+ }
+
+ skip += OWNER_PAGE_SIZE
+ }
+
+ return owners
+}
+
+const buildSnapshotResponse = async (): Promise => {
+ const now = Math.floor(Date.now() / 1000)
+ const chainSnapshots = await Promise.all(
+ PUBLIC_DEFAULT_CHAINS.map(async (chain) => {
+ const subgraphUrl = PUBLIC_SUBGRAPH_URL.get(chain.id)
+ if (!subgraphUrl) {
+ return { daos: [], uniqueOwners: null as Set | null }
+ }
+
+ const [daos, uniqueOwners] = await Promise.all([
+ fetchChainSnapshot(subgraphUrl),
+ fetchUniqueOwnersForChain(subgraphUrl).catch((error) => {
+ console.error(
+ `About snapshot unique owner fetch failed for chain ${chain.id}:`,
+ error
+ )
+ return null
+ }),
+ ])
+
+ return { daos, uniqueOwners }
+ })
+ )
+
+ const daos = chainSnapshots.flatMap((snapshot) => snapshot.daos)
+
+ const totalDaos = daos.length
+ const totalAuctionSales = daos.reduce((total, dao) => {
+ const sales = Number(formatEther(BigInt(dao.totalAuctionSales || '0')))
+ return total + sales
+ }, 0)
+ const activeAuctions = daos.filter(
+ (dao) =>
+ dao.currentAuction &&
+ !dao.currentAuction.settled &&
+ Number(dao.currentAuction.endTime) > now
+ ).length
+ const totalProposals = daos.reduce(
+ (total, dao) => total + Number(dao.proposalCount || 0),
+ 0
+ )
+ const uniqueOwners = new Set()
+
+ for (const snapshot of chainSnapshots) {
+ if (!snapshot.uniqueOwners) continue
+
+ for (const owner of snapshot.uniqueOwners) {
+ uniqueOwners.add(owner)
+ }
+ }
+
+ const allOwnerScansSucceeded = chainSnapshots.every((snapshot) => snapshot.uniqueOwners)
+ const fallbackTotalMembers = daos.reduce(
+ (total, dao) => total + Number(dao.ownerCount || 0),
+ 0
+ )
+
+ const totalMembers = allOwnerScansSucceeded ? uniqueOwners.size : fallbackTotalMembers
+ const totalTokens = daos.reduce((total, dao) => total + Number(dao.totalSupply || 0), 0)
+
+ return {
+ stats: [
+ {
+ id: 'daos',
+ label: 'DAOs launched',
+ value: compactNumber(totalDaos),
+ detail: 'Live DAO count indexed across Builder-supported public networks.',
+ icon: '🚀',
+ },
+ {
+ id: 'treasury',
+ label: 'Auction value raised',
+ value: compactEth(totalAuctionSales),
+ detail:
+ 'Cumulative native-token auction sales flowing into community treasuries.',
+ icon: '💰',
+ },
+ {
+ id: 'auctions',
+ label: 'Active auctions',
+ value: compactNumber(activeAuctions),
+ detail: 'Current live auctions still accepting bids across the ecosystem.',
+ icon: '⏰',
+ },
+ {
+ id: 'proposals',
+ label: 'Governance proposals',
+ value: compactNumber(totalProposals),
+ detail: 'Total proposals created across DAOs using Builder governance.',
+ icon: '📜',
+ },
+ {
+ id: 'members',
+ label: 'Members holding tokens',
+ value: compactNumber(totalMembers),
+ detail: 'Distinct DAO token holders participating across Builder communities.',
+ icon: '👥',
+ },
+ {
+ id: 'tokens',
+ label: 'Tokens auctioned',
+ value: compactNumber(totalTokens),
+ detail: 'Member tokens minted and distributed through recurring auctions.',
+ icon: '🔥',
+ },
+ ],
+ }
+}
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ if (req.method !== 'GET') {
+ return res.status(405).json({ error: 'Method not allowed' })
+ }
+
+ try {
+ const payload = await buildSnapshotResponse()
+ const { maxAge, swr } = CACHE_TIMES.EXPLORE
+
+ res.setHeader(
+ 'Cache-Control',
+ `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}`
+ )
+
+ return res.status(200).json(payload)
+ } catch (error) {
+ console.error('About snapshot error:', error)
+ return res.status(500).json({ error: 'Failed to load about snapshot stats' })
+ }
+}
+
+export default withCors()(
+ withRateLimit({
+ maxRequests: 60,
+ windowSeconds: 60,
+ keyPrefix: 'about:snapshot',
+ })(handler)
+)
diff --git a/apps/web/src/pages/api/ai/generateTxSummary.ts b/apps/web/src/pages/api/ai/generateTxSummary.ts
index 76e3c09d0..3f9504d1e 100644
--- a/apps/web/src/pages/api/ai/generateTxSummary.ts
+++ b/apps/web/src/pages/api/ai/generateTxSummary.ts
@@ -1,4 +1,3 @@
-import { gateway } from '@ai-sdk/gateway'
import { CACHE_TIMES } from '@buildeross/constants/cacheTimes'
import { PUBLIC_ALL_CHAINS } from '@buildeross/constants/chains'
import type {
@@ -14,13 +13,9 @@ import type { DecodedEscrowData } from '@buildeross/utils/escrow'
import { formatBpsValue, formatTokenValue } from '@buildeross/utils/formatArgs'
import { walletSnippet } from '@buildeross/utils/helpers'
import * as Sentry from '@sentry/nextjs'
-import { generateText } from 'ai'
import { NextApiRequest, NextApiResponse } from 'next'
-import { getRedisConnection } from 'src/services/redisConnection'
+import { AI_MODEL, generateCachedAiText } from 'src/utils/api/ai/summaries'
import { withRateLimit } from 'src/utils/api/rateLimit'
-import { keccak256, toHex } from 'viem'
-
-const AI_MODEL = process.env.AI_MODEL || 'openai/gpt-4-turbo'
type RequestBody = {
chainId: CHAIN_ID
@@ -34,14 +29,6 @@ type RequestBody = {
bundleContext?: ProposalTransactionBundleContext
}
-const safeStringify = (v: unknown) =>
- JSON.stringify(v, (_k, val) => (typeof val === 'bigint' ? val.toString() : val))
-
-const getCacheKey = (data: RequestBody, model: string) => {
- const hash = keccak256(toHex(`${safeStringify(data)}:${model}`))
- return `ai:txSummary:${hash}`
-}
-
/**
* Recursively traverses transaction arguments and formats
* token-related fields or BPS percentage fields.
@@ -318,27 +305,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const prompt = generatePrompt(requestData)
const model = AI_MODEL
- const redisConnection = getRedisConnection()
- const cacheKey = getCacheKey(requestData, model)
-
- // Check cache first
- let cachedText = await redisConnection?.get(cacheKey)
-
- if (cachedText) {
- return res.status(200).json({ text: cachedText })
- }
-
- // Generate new text if not in cache
- const result = await generateText({
- model: gateway(model),
+ const text = await generateCachedAiText({
+ namespace: 'ai:txSummary',
+ data: requestData,
prompt,
- abortSignal: AbortSignal.timeout(30000),
+ model,
})
- // Cache the generated text for 30 days
- await redisConnection?.setex(cacheKey, 60 * 60 * 24 * 30, result.text)
-
- res.status(200).json({ text: result.text })
+ res.status(200).json({ text })
} catch (error) {
console.error(`Error generating transaction summary:`, error)
diff --git a/apps/web/src/pages/api/migrated.ts b/apps/web/src/pages/api/migrated.ts
index 3a67fa899..df2589036 100644
--- a/apps/web/src/pages/api/migrated.ts
+++ b/apps/web/src/pages/api/migrated.ts
@@ -14,7 +14,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const data = await Promise.all(
L2_CHAINS.map((chainId) => {
- const deployer = L2_MIGRATION_DEPLOYER[chainId]
+ const deployer =
+ L2_MIGRATION_DEPLOYER[chainId as keyof typeof L2_MIGRATION_DEPLOYER]
if (deployer === zeroAddress) return []
return readContract(serverConfig, {
diff --git a/apps/web/src/pages/dao/[network]/[token]/index.tsx b/apps/web/src/pages/dao/[network]/[token]/index.tsx
index 9ee149ea7..36346da41 100644
--- a/apps/web/src/pages/dao/[network]/[token]/index.tsx
+++ b/apps/web/src/pages/dao/[network]/[token]/index.tsx
@@ -43,6 +43,9 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress
const { address: signerAddress } = useAccount()
const { addresses } = useDaoStore()
const chain = useChainStore((x) => x.chain)
+ const chainIdKey = chain.id as keyof typeof MERKLE_RESERVE_MINTER
+ const merkleMinter = MERKLE_RESERVE_MINTER[chainIdKey]
+ const redeemMinter = ERC721_REDEEM_MINTER[chainIdKey]
const auctionContractParams = {
abi: auctionAbi,
@@ -56,26 +59,40 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress
chainId: chain.id,
}
+ const contracts = [
+ { ...auctionContractParams, functionName: 'owner' as const },
+ { ...tokenContractParams, functionName: 'remainingTokensInReserve' as const },
+ ...(merkleMinter
+ ? ([
+ {
+ ...tokenContractParams,
+ functionName: 'minter' as const,
+ args: [merkleMinter] as const,
+ },
+ ] as const)
+ : []),
+ ...(redeemMinter
+ ? ([
+ {
+ ...tokenContractParams,
+ functionName: 'minter' as const,
+ args: [redeemMinter] as const,
+ },
+ ] as const)
+ : []),
+ ]
+
const { data: contractData } = useReadContracts({
allowFailure: false,
- contracts: [
- { ...auctionContractParams, functionName: 'owner' },
- { ...tokenContractParams, functionName: 'remainingTokensInReserve' },
- {
- ...tokenContractParams,
- functionName: 'minter',
- args: [MERKLE_RESERVE_MINTER[chain.id]],
- },
- {
- ...tokenContractParams,
- functionName: 'minter',
- args: [ERC721_REDEEM_MINTER[chain.id]],
- },
- ] as const,
+ contracts,
})
- const [owner, remainingTokensInReserve, isMerkleReserveMinter, isERC721RedeemMinter] =
- unpackOptionalArray(contractData, 4)
+ const [owner, remainingTokensInReserve, ...minterStatus] = (unpackOptionalArray(
+ contractData,
+ contracts.length
+ ) ?? []) as [unknown, bigint | undefined, ...unknown[]]
+ const isMerkleReserveMinter = merkleMinter ? !!minterStatus[0] : false
+ const isERC721RedeemMinter = redeemMinter ? !!minterStatus[merkleMinter ? 1 : 0] : false
// Separate read for signer minter status
const { data: isSignerMinter } = useReadContract({
@@ -112,13 +129,13 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress
const handleMinterEnabled = React.useCallback(
async (minterAddress: AddressType) => {
// Navigate to appropriate tab when minter is enabled
- if (minterAddress === MERKLE_RESERVE_MINTER[chain.id]) {
+ if (merkleMinter && minterAddress === merkleMinter) {
await openTab('merkle-reserve')
- } else if (minterAddress === ERC721_REDEEM_MINTER[chain.id]) {
+ } else if (redeemMinter && minterAddress === redeemMinter) {
await openTab('erc721-redeem')
}
},
- [chain.id, openTab]
+ [merkleMinter, redeemMinter, openTab]
)
const openTokenPage = React.useCallback(
diff --git a/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx b/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx
index 00b27bd06..9f9fe04c9 100644
--- a/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx
+++ b/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx
@@ -51,6 +51,7 @@ import { notFoundWrap } from 'src/styles/404.css'
import * as styles from 'src/styles/create.css'
import { getAddress, isAddress, isAddressEqual } from 'viem'
import { useAccount, useReadContract } from 'wagmi'
+import { useShallow } from 'zustand/shallow'
const createSelectOption = (type: TransactionType) => ({
value: type,
@@ -149,23 +150,41 @@ const CreateProposalPage: NextPageWithLayout = () => {
const addresses = useDaoStore((x) => x.addresses)
const { auction, token } = addresses
const chain = useChainStore((x) => x.chain)
- const transactionType = useProposalStore((x) => x.transactionType)
- const setTransactionType = useProposalStore((x) => x.setTransactionType)
- const resetTransactionType = useProposalStore((x) => x.resetTransactionType)
- const transactions = useProposalStore((x) => x.transactions)
- const title = useProposalStore((x) => x.title)
- const summary = useProposalStore((x) => x.summary)
- const representedAddress = useProposalStore((x) => x.representedAddress)
- const discussionUrl = useProposalStore((x) => x.discussionUrl)
- const representedAddressEnabled = useProposalStore((x) => x.representedAddressEnabled)
- const setTitle = useProposalStore((x) => x.setTitle)
- const setSummary = useProposalStore((x) => x.setSummary)
- const setRepresentedAddress = useProposalStore((x) => x.setRepresentedAddress)
- const setDiscussionUrl = useProposalStore((x) => x.setDiscussionUrl)
- const setRepresentedAddressEnabled = useProposalStore(
- (x) => x.setRepresentedAddressEnabled
+ const {
+ transactionType,
+ setTransactionType,
+ resetTransactionType,
+ transactions,
+ title,
+ summary,
+ representedAddress,
+ discussionUrl,
+ representedAddressEnabled,
+ setTitle,
+ setSummary,
+ setRepresentedAddress,
+ setDiscussionUrl,
+ setRepresentedAddressEnabled,
+ clearProposal,
+ } = useProposalStore(
+ useShallow((state) => ({
+ transactionType: state.transactionType,
+ setTransactionType: state.setTransactionType,
+ resetTransactionType: state.resetTransactionType,
+ transactions: state.transactions,
+ title: state.title,
+ summary: state.summary,
+ representedAddress: state.representedAddress,
+ discussionUrl: state.discussionUrl,
+ representedAddressEnabled: state.representedAddressEnabled,
+ setTitle: state.setTitle,
+ setSummary: state.setSummary,
+ setRepresentedAddress: state.setRepresentedAddress,
+ setDiscussionUrl: state.setDiscussionUrl,
+ setRepresentedAddressEnabled: state.setRepresentedAddressEnabled,
+ clearProposal: state.clearProposal,
+ }))
)
- const clearProposal = useProposalStore((x) => x.clearProposal)
const initialStageFromQuery = query?.stage === 'transactions' ? 'transactions' : 'draft'
const [createStage, setCreateStage] = React.useState<'draft' | 'transactions'>(() =>
diff --git a/apps/web/src/utils/api/ai/summaries.ts b/apps/web/src/utils/api/ai/summaries.ts
new file mode 100644
index 000000000..c8c3fb785
--- /dev/null
+++ b/apps/web/src/utils/api/ai/summaries.ts
@@ -0,0 +1,115 @@
+import { gateway } from '@ai-sdk/gateway'
+import { generateText } from 'ai'
+import { getRedisConnection } from 'src/services/redisConnection'
+
+export const AI_MODEL = process.env.AI_MODEL || 'openai/gpt-4-turbo'
+const DEFAULT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 30
+const MAX_DAO_DESCRIPTION_LENGTH = 2200
+
+const safeStringify = (value: unknown) =>
+ JSON.stringify(value, (_key, item) =>
+ typeof item === 'bigint' ? item.toString() : item
+ )
+
+export const getAiCacheKey = (
+ namespace: string,
+ data: unknown,
+ model: string = AI_MODEL
+) => {
+ const payload = `${namespace}:${model}:${safeStringify(data)}`
+ let hash = 2166136261
+
+ for (let i = 0; i < payload.length; i += 1) {
+ hash ^= payload.charCodeAt(i)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return `${namespace}:${(hash >>> 0).toString(16)}`
+}
+
+export const generateCachedAiText = async ({
+ namespace,
+ data,
+ prompt,
+ model = AI_MODEL,
+ ttlSeconds = DEFAULT_CACHE_TTL_SECONDS,
+}: {
+ namespace: string
+ data: unknown
+ prompt: string
+ model?: string
+ ttlSeconds?: number
+}) => {
+ const redisConnection = getRedisConnection()
+ const cacheKey = getAiCacheKey(namespace, data, model)
+
+ if (!redisConnection) {
+ console.warn(
+ `[ai-cache] Redis unavailable for namespace=${namespace} key=${cacheKey}; skipping cache.`
+ )
+ }
+
+ const cachedText = redisConnection ? await redisConnection.get(cacheKey) : null
+
+ if (cachedText) {
+ return cachedText
+ }
+
+ const result = await generateText({
+ model: gateway(model),
+ prompt,
+ abortSignal: AbortSignal.timeout(30000),
+ })
+
+ const text = result.text.trim().replace(/\s+/g, ' ')
+
+ if (redisConnection) {
+ await redisConnection.setex(cacheKey, ttlSeconds, text)
+ }
+
+ return text
+}
+
+const trimDescription = (description: string, maxChars = MAX_DAO_DESCRIPTION_LENGTH) =>
+ description.trim().replace(/\s+/g, ' ').slice(0, maxChars)
+
+const sanitizeDescription = (description: string) =>
+ description.replace(/###|assistant\s*:|system\s*:|user\s*:/gi, '')
+
+const buildDaoDescriptionPrompt = (
+ description: string
+) => `You are writing concise directory copy for a DAO platform.
+Summarize the DAO description below into 1-2 short sentences for a directory card.
+
+Writing Rules:
+- Keep it factual, clear, and neutral.
+- Do not mention the DAO name.
+- Do not use hype, markdown, bullet points, or line breaks.
+- Prefer concrete activity, purpose, or community focus over abstract framing.
+- Maximum 2 short sentences.
+
+DAO description:
+${trimDescription(description)}
+
+Final Instruction:
+Respond with only the 1-2 sentence summary.`
+
+export const summarizeDaoDescription = async (description: string) => {
+ if (description.length > MAX_DAO_DESCRIPTION_LENGTH) {
+ console.warn(
+ `[ai-cache] DAO description exceeded ${MAX_DAO_DESCRIPTION_LENGTH} chars; truncating before summarization.`
+ )
+ }
+
+ const cleaned = sanitizeDescription(trimDescription(description))
+
+ if (!cleaned.trim()) {
+ throw new Error('DAO description is empty after trimming/sanitization')
+ }
+
+ return generateCachedAiText({
+ namespace: 'ai:daoDescription',
+ data: { description: cleaned },
+ prompt: buildDaoDescriptionPrompt(cleaned),
+ })
+}
diff --git a/packages/dao-ui/src/components/Gallery/Gallery.tsx b/packages/dao-ui/src/components/Gallery/Gallery.tsx
index 2a5b515ca..f1bbaa36c 100644
--- a/packages/dao-ui/src/components/Gallery/Gallery.tsx
+++ b/packages/dao-ui/src/components/Gallery/Gallery.tsx
@@ -127,6 +127,7 @@ const MintWidgetModal: React.FC = ({
editionSize={drop.editionSize}
maxPerAddress={parseInt(drop.maxSalePurchasePerAddress)}
onMintSuccess={handleCloseModal}
+ unstyledContainer
/>
diff --git a/packages/feed-ui/src/Modals/MintModal.tsx b/packages/feed-ui/src/Modals/MintModal.tsx
index d0b1eb67c..88d1d06a3 100644
--- a/packages/feed-ui/src/Modals/MintModal.tsx
+++ b/packages/feed-ui/src/Modals/MintModal.tsx
@@ -74,6 +74,7 @@ export const MintModal: React.FC = ({
editionSize={editionSize}
maxPerAddress={maxPerAddress}
onMintSuccess={onSuccessMint}
+ unstyledContainer
/>
diff --git a/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts b/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts
index 4e39eca17..310591805 100644
--- a/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts
+++ b/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts
@@ -2,6 +2,55 @@ import { ProposalState } from '@buildeross/sdk/contract'
import { fromSeconds } from '@buildeross/utils/helpers'
import { theme } from '@buildeross/zord'
+export type ProposalStateColorStyle = {
+ borderColor: string
+ color: string
+}
+
+export const proposalStateColorStyles: Record = {
+ [ProposalState.Pending]: {
+ borderColor: theme.colors.warningDisabled,
+ color: theme.colors.warning,
+ },
+ [ProposalState.Active]: {
+ borderColor: theme.colors.focusRing,
+ color: theme.colors.focusRing,
+ },
+ [ProposalState.Canceled]: {
+ borderColor: theme.colors.background2,
+ color: theme.colors.text4,
+ },
+ [ProposalState.Defeated]: {
+ borderColor: theme.colors.negativeDisabled,
+ color: theme.colors.negative,
+ },
+ [ProposalState.Succeeded]: {
+ borderColor: theme.colors.positiveDisabled,
+ color: theme.colors.positive,
+ },
+ [ProposalState.Queued]: {
+ borderColor: theme.colors.neutral,
+ color: theme.colors.secondary,
+ },
+ [ProposalState.Expired]: {
+ borderColor: theme.colors.background2,
+ color: theme.colors.text4,
+ },
+ [ProposalState.Executed]: {
+ borderColor: theme.colors.positiveDisabled,
+ color: theme.colors.positive,
+ },
+ [ProposalState.Vetoed]: {
+ borderColor: theme.colors.background2,
+ color: theme.colors.text4,
+ },
+}
+
+export const getProposalStateColorStyle = (
+ state: ProposalState
+): ProposalStateColorStyle =>
+ proposalStateColorStyles[state] || proposalStateColorStyles[ProposalState.Expired]
+
export function formatTime(
timediff: number,
affix: string,
@@ -58,39 +107,5 @@ export function parseState(state: ProposalState) {
}
export function parseBgColor(state: ProposalState) {
- switch (state) {
- case ProposalState.Pending:
- return {
- borderColor: theme.colors.warningDisabled,
- color: theme.colors.warning,
- }
- case ProposalState.Active:
- return {
- borderColor: '#0085FF',
- color: '#0085FF',
- }
- case ProposalState.Succeeded:
- return {
- borderColor: theme.colors.positiveDisabled,
- color: theme.colors.positive,
- }
- case ProposalState.Defeated:
- return {
- borderColor: theme.colors.negativeDisabled,
- color: theme.colors.negative,
- }
- case ProposalState.Executed:
- return {
- borderColor: theme.colors.positiveDisabled,
- color: theme.colors.positive,
- }
- case ProposalState.Queued:
- return {
- borderColor: theme.colors.neutral,
- color: theme.colors.secondary,
- }
- case ProposalState.Expired:
- default:
- return { borderColor: theme.colors.background2, color: theme.colors.text4 }
- }
+ return getProposalStateColorStyle(state)
}
diff --git a/packages/proposal-ui/src/components/ProposalStatus/index.ts b/packages/proposal-ui/src/components/ProposalStatus/index.ts
index 6e7bb6159..b91603830 100644
--- a/packages/proposal-ui/src/components/ProposalStatus/index.ts
+++ b/packages/proposal-ui/src/components/ProposalStatus/index.ts
@@ -1 +1,2 @@
export * from './ProposalStatus'
+export * from './ProposalStatus.helper'
diff --git a/packages/proposal-ui/src/index.ts b/packages/proposal-ui/src/index.ts
index ef98a54ff..698cd7956 100644
--- a/packages/proposal-ui/src/index.ts
+++ b/packages/proposal-ui/src/index.ts
@@ -1,2 +1,3 @@
export * from './components'
+export * from './components/ProposalStatus/ProposalStatus.helper'
export * from './constants'
diff --git a/packages/sdk/src/subgraph/sdk.generated.ts b/packages/sdk/src/subgraph/sdk.generated.ts
index 386ab26d5..dd4deb4df 100644
--- a/packages/sdk/src/subgraph/sdk.generated.ts
+++ b/packages/sdk/src/subgraph/sdk.generated.ts
@@ -30,6 +30,14 @@ export type Scalars = {
Timestamp: { input: any; output: any }
}
+/** Indicates whether the current, partially filled bucket should be included in the response. Defaults to `exclude` */
+export enum Aggregation_Current {
+ /** Exclude the current, partially filled bucket from the response */
+ Exclude = 'exclude',
+ /** Include the current, partially filled bucket in the response */
+ Include = 'include',
+}
+
export enum Aggregation_Interval {
Day = 'day',
Hour = 'hour',
@@ -1283,10 +1291,8 @@ export type ClankerToken_Filter = {
extensionsSupply_not?: InputMaybe
extensionsSupply_not_in?: InputMaybe>
extensions_contains?: InputMaybe>
- extensions_contains_nocase?: InputMaybe>
extensions_not?: InputMaybe>
extensions_not_contains?: InputMaybe>
- extensions_not_contains_nocase?: InputMaybe>
holders_?: InputMaybe
id?: InputMaybe
id_gt?: InputMaybe
@@ -4436,10 +4442,8 @@ export type Proposal_Filter = {
snapshotBlockNumber_not_in?: InputMaybe>
targets?: InputMaybe>
targets_contains?: InputMaybe>
- targets_contains_nocase?: InputMaybe>
targets_not?: InputMaybe>
targets_not_contains?: InputMaybe>
- targets_not_contains_nocase?: InputMaybe>
timeCreated?: InputMaybe
timeCreated_gt?: InputMaybe
timeCreated_gte?: InputMaybe
@@ -4481,10 +4485,8 @@ export type Proposal_Filter = {
updates_?: InputMaybe
values?: InputMaybe>
values_contains?: InputMaybe>
- values_contains_nocase?: InputMaybe>
values_not?: InputMaybe>
values_not_contains?: InputMaybe>
- values_not_contains_nocase?: InputMaybe>
vetoTransactionHash?: InputMaybe
vetoTransactionHash_contains?: InputMaybe
vetoTransactionHash_gt?: InputMaybe
diff --git a/packages/ui/src/DropMintWidget/DropMintWidget.css.ts b/packages/ui/src/DropMintWidget/DropMintWidget.css.ts
index de6c1767f..b17bcc65c 100644
--- a/packages/ui/src/DropMintWidget/DropMintWidget.css.ts
+++ b/packages/ui/src/DropMintWidget/DropMintWidget.css.ts
@@ -5,7 +5,17 @@ export const widgetContainer = style([
atoms({
p: 'x6',
borderRadius: 'phat',
- backgroundColor: 'background2',
+ backgroundColor: 'background1',
+ borderColor: 'border',
+ borderStyle: 'solid',
+ borderWidth: 'thin',
+ }),
+])
+
+export const widgetContainerUnstyled = style([
+ atoms({
+ p: 'x0',
+ borderRadius: 'curved',
}),
])
@@ -20,8 +30,8 @@ export const mintInputContainer = style([
export const mintInput = style({
fontSize: '1.125rem',
fontWeight: 600,
- border: '2px solid transparent',
- backgroundColor: 'transparent',
+ border: `2px solid ${vars.color.background1}`,
+ backgroundColor: vars.color.background2,
textAlign: 'center',
width: '100%',
flex: 1,
@@ -34,6 +44,10 @@ export const mintInput = style({
borderColor: vars.color.border,
backgroundColor: vars.color.background1,
},
+ '&:focus-visible': {
+ outline: `2px solid ${vars.color.focusRing}`,
+ outlineOffset: '2px',
+ },
'&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': {
WebkitAppearance: 'none',
margin: 0,
@@ -46,7 +60,7 @@ export const mintInput = style({
export const quantityInputWrapper = style([
atoms({
- backgroundColor: 'background1',
+ backgroundColor: 'transparent',
borderRadius: 'curved',
p: 'x2',
}),
@@ -78,7 +92,10 @@ export const quantityButton = style({
selectors: {
'&:disabled': {
cursor: 'not-allowed',
- opacity: 0.4,
+ opacity: 1,
+ color: vars.color.text3,
+ borderColor: vars.color.border,
+ backgroundColor: vars.color.background1,
},
},
})
diff --git a/packages/ui/src/DropMintWidget/DropMintWidget.tsx b/packages/ui/src/DropMintWidget/DropMintWidget.tsx
index bdcc197ea..f13b7f64b 100644
--- a/packages/ui/src/DropMintWidget/DropMintWidget.tsx
+++ b/packages/ui/src/DropMintWidget/DropMintWidget.tsx
@@ -17,6 +17,7 @@ import {
quantityInputWrapper,
successMessage,
widgetContainer,
+ widgetContainerUnstyled,
} from './DropMintWidget.css'
export interface DropMintWidgetProps {
@@ -32,6 +33,7 @@ export interface DropMintWidgetProps {
editionSize?: string
maxPerAddress?: number
onMintSuccess?: (txHash: string) => void
+ unstyledContainer?: boolean
}
export const DropMintWidget = ({
@@ -47,6 +49,7 @@ export const DropMintWidget = ({
// editionSize,
maxPerAddress,
onMintSuccess,
+ unstyledContainer = false,
}: DropMintWidgetProps) => {
const [quantity, setQuantity] = useState(1)
const [comment, setComment] = useState('')
@@ -119,7 +122,10 @@ export const DropMintWidget = ({
}
return (
-
+
{/* Price */}
@@ -170,15 +176,18 @@ export const DropMintWidget = ({
Quantity
-
- -
-
+ {quantity > 1 ? (
+
+ -
+
+ ) : (
+
+ )}
= maxPerAddress : false}
diff --git a/packages/zord/src/elements/Button.css.ts b/packages/zord/src/elements/Button.css.ts
index a7cf5b51e..55f7ffe18 100644
--- a/packages/zord/src/elements/Button.css.ts
+++ b/packages/zord/src/elements/Button.css.ts
@@ -116,6 +116,10 @@ export const buttonVariants = {
cursor: 'pointer',
backgroundColor: vars.color.accentHover,
},
+ 'html[data-theme-mode="dark"] &:not([disabled]):hover': {
+ backgroundColor: vars.color.neutralHover,
+ borderColor: vars.color.neutralHover,
+ },
},
},
atoms({
@@ -141,6 +145,27 @@ export const buttonVariants = {
backgroundColor: 'background2',
}),
]),
+ secondaryOutline: style([
+ {
+ selectors: {
+ '&[disabled]': {
+ color: vars.color.onNeutralDisabled,
+ borderColor: vars.color.neutralDisabled,
+ backgroundColor: 'transparent',
+ },
+ '&:not([disabled]):hover': {
+ cursor: 'pointer',
+ backgroundColor: vars.color.background2,
+ },
+ },
+ },
+ atoms({
+ color: 'primary',
+ borderColor: 'border',
+ borderWidth: 'normal',
+ backgroundColor: 'transparent',
+ }),
+ ]),
secondaryAccent: style([
{
selectors: {
@@ -280,7 +305,7 @@ export const buttonVariants = {
},
'&:hover, &:not([disabled]):hover': {
cursor: 'pointer',
- backgroundColor: vars.color.ghostHover,
+ backgroundColor: vars.color.background2,
},
},
},
diff --git a/packages/zord/src/elements/Button.tsx b/packages/zord/src/elements/Button.tsx
index 6d4096d2f..456764906 100644
--- a/packages/zord/src/elements/Button.tsx
+++ b/packages/zord/src/elements/Button.tsx
@@ -36,6 +36,7 @@ export interface ButtonProps extends FlexProps {
variant?:
| 'primary'
| 'secondary'
+ | 'secondaryOutline'
| 'secondaryAccent'
| 'positive'
| 'destructive'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7689ea952..82a95c3cd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -212,6 +212,9 @@ importers:
framer-motion:
specifier: ^12.12.1
version: 12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ graphql-request:
+ specifier: ^7.1.2
+ version: 7.3.0(graphql@16.11.0)
ioredis:
specifier: ^5.8.1
version: 5.8.1
@@ -233,6 +236,9 @@ importers:
react-mde:
specifier: ^11.5.0
version: 11.5.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ remove-markdown:
+ specifier: ^0.6.3
+ version: 0.6.3
sablier:
specifier: ^2.0.1
version: 2.0.1(@coral-xyz/anchor@0.30.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(viem@2.47.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))
@@ -10638,6 +10644,9 @@ packages:
remedial@1.0.8:
resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==}
+ remove-markdown@0.6.3:
+ resolution: {integrity: sha512-Qvp2p0Q1irE7AaJO7QemJe04HdObHylJrG+q4hszvPlYp7q4EvfINpEIaIEFdB+3XTDp1h6fiyT60ae00gmRow==}
+
remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
@@ -15339,10 +15348,10 @@ snapshots:
'@metamask/superstruct': 3.2.1
'@noble/hashes': 1.8.0
'@scure/base': 1.2.6
- '@types/debug': 4.1.12
+ '@types/debug': 4.1.13
debug: 4.4.3(supports-color@8.1.1)
pony-cause: 2.1.11
- semver: 7.7.3
+ semver: 7.7.4
uuid: 9.0.1
transitivePeerDependencies:
- supports-color
@@ -24580,6 +24589,8 @@ snapshots:
remedial@1.0.8: {}
+ remove-markdown@0.6.3: {}
+
remove-trailing-separator@1.1.0: {}
remove-trailing-spaces@1.0.9: {}
@@ -24670,7 +24681,7 @@ snapshots:
rlp@2.2.7:
dependencies:
- bn.js: 5.2.2
+ bn.js: 5.2.3
rollup@4.52.4:
dependencies:
diff --git a/turbo.json b/turbo.json
index 03c1fecc0..16f0579fd 100644
--- a/turbo.json
+++ b/turbo.json
@@ -9,6 +9,8 @@
"ETHERSCAN_API_KEY",
"AI_GATEWAY_API_KEY",
"AI_MODEL",
+ "OPENAI_API_KEY",
+ "NEYNAR_API_KEY",
"NEXT_PUBLIC_DISABLE_AI_SUMMARY",
"NEXT_PUBLIC_DISABLE_TENDERLY_SIMULATION",
"NEXT_PUBLIC_PINATA_GATEWAY",