纳斯达克开盘钟声敲响那一刻,Bending Spoons的估值定格在超过180亿美元,收盘时股价又涨了40%。这家总部在米兰、成立13年的公司,靠着收购Meetup、Eventbrite、Vimeo、WeTransfer、Evernote这些"曾经红过现在衰败"的互联网品牌起家,如今成了AOL的母公司,也成了今年科技股里少有的高开高走案例。

联合创始人兼首席产品官Matteo Danieli在IPO当天给出的解释很讨巧:"我们发展出一种执念,尽量减少运气在增长和成功中扮演的角色。"这句话写进了公司提交给SEC的F-1文件,还专门开了一章叫"AI before it was cool"。故事讲得漂亮,但真正的问题是:这套系统化打法,到底是经得起验证的能力,还是IPO窗口期精心打磨的叙事。

从失败项目里长出来的方法论

Danieli口中的"减少运气"哲学,源头是一次失败。Bending Spoons四位创始人早年做过一个叫Evertale的项目,用当时还叫"机器学习"、现在叫AI的技术自动生成用户生活日记。产品没做起来,但团队从中悟出一个道理:创业者的才华和从0到1的成功之间,并不总是正相关,运气占了很大权重。

这段经历直接塑造了Bending Spoons后来的收购逻辑——与其赌一个新点子能不能跑通产品市场匹配,不如收购已经验证过市场、只是运营衰败的品牌,再用数据、AI和标准化流程把它们重新盘活。公司在早期融资时没少碰壁,"你疯了"是常见反馈,这也是为什么公司的标语是"Impossible. Maybe."。

  • 结论.把"运气"从产品验证环节移除,靠收购既有用户基础的老品牌来对冲从0到1的不确定性,这才是"减少运气"真正的操作含义,而不是空泛的管理鸡汤。

人均营收的过山车,才是真正的悬念

F-1文件里最值得细看的一组数字,是人均营收的变化:2023年每位员工创造112万美元营收,到2025年涨到257万美元,Danieli把这归功于AI带来的效率跃升。但到了2026年第一季度,这个数字回落到97万美元。

公司同期披露全年营收13.1亿美元,第一季度营收6.01亿美元。单季数字放大到全年口径本身就有波动性,人均营收环比下滑也未必意味着增长故事破产。但这恰恰是IPO后每个季度都要面对的现实:私募轮次可以用叙事说服投资人,公开市场要用连续几个季度的数字说话。

人均营收变化:AI叙事的关键数字 2023年 112万 美元/人 2025年 257万 美元/人 · AI效率高点 2026年Q1 97万 美元/人 · 单季回落 数据来源:Bending Spoons F-1招股说明书 { const threshold = Math.max(88, Math.min(180, Math.round(window.innerHeight * 0.16))) quicknav.classList.toggle('is-visible', (window.scrollY || window.pageYOffset || 0) > threshold) } sync_quicknav() window.addEventListener('scroll', sync_quicknav, { passive: true }) window.addEventListener('resize', sync_quicknav) const share_btn = quicknav.querySelector('[data-article-share]') if (share_btn) { share_btn.addEventListener('click', async () => { const url = window.location.href const title = document.title try { if (navigator.share) { await navigator.share({ title, url }); return } if (navigator.clipboard) { await navigator.clipboard.writeText(url) share_btn.classList.add('is-copied') setTimeout(() => share_btn.classList.remove('is-copied'), 1200) } } catch (err) { /* 用户取消或不支持,忽略 */ } }) } } const maps = document.querySelectorAll('[data-article-mindmap]') if (!maps.length) return const clamp = (value, min, max) => Math.min(max, Math.max(min, value)) for (const viewport of maps) { const frame = viewport.closest('.article-mindmap-frame') const stage = viewport.querySelector('[data-mindmap-stage]') const canvas = viewport.querySelector('[data-mindmap-canvas]') const layout = viewport.querySelector('[data-mindmap-layout]') const toolbar = frame?.querySelector('.article-mindmap-toolbar') if (!stage || !canvas || !layout || !toolbar) continue let scale = 1 let fit_scale_value = 1 let min_scale = 0.28 let max_scale = 1.8 let natural_width = 0 let natural_height = 0 let pinch_start_distance = 0 let pinch_start_scale = 1 let drag_start_x = 0 let drag_start_y = 0 let drag_start_left = 0 let drag_start_top = 0 let drag_enabled = false const active_pointers = new Map() function is_mobile() { return window.matchMedia('(max-width: 640px)').matches } function get_screen_height() { const visual_height = window.visualViewport?.height if (Number.isFinite(visual_height) && visual_height > 0) return visual_height return window.innerHeight || document.documentElement.clientHeight || 0 } function sync_viewport_max_height() { viewport.style.maxHeight = 'none' } function sync_buttons() { const reset_button = toolbar.querySelector('[data-mindmap-action="reset"]') if (reset_button) { reset_button.textContent = Math.round(scale * 100) + '%' } } function measure() { sync_viewport_max_height() canvas.style.transform = 'scale(1)' canvas.style.width = '' canvas.style.height = '' stage.style.width = '' stage.style.height = '' natural_width = Math.ceil(layout.offsetWidth) natural_height = Math.ceil(layout.offsetHeight) } function apply_scale(next_scale, { preserve_center = false } = {}) { if (!natural_width || !natural_height) measure() const previous_scale = scale scale = clamp(next_scale, min_scale, max_scale) const viewport_width = viewport.clientWidth || 1 const viewport_height = viewport.clientHeight || 1 const center_x = preserve_center ? (viewport.scrollLeft + viewport_width / 2) / Math.max(previous_scale, 0.001) : 0 const center_y = preserve_center ? (viewport.scrollTop + viewport_height / 2) / Math.max(previous_scale, 0.001) : 0 const scaled_width = Math.ceil(natural_width * scale) const scaled_height = Math.ceil(natural_height * scale) const fits_width = scaled_width <= viewport_width + 1 const fits_height = scaled_height <= viewport_height + 1 canvas.style.transform = 'scale(' + scale + ')' canvas.style.width = scaled_width + 'px' canvas.style.height = scaled_height + 'px' stage.style.width = (fits_width ? viewport_width : scaled_width) + 'px' stage.style.height = scaled_height + 'px' viewport.classList.toggle('is-contained', fits_width && fits_height) if (fits_width) { viewport.scrollLeft = 0 } if (fits_height) { viewport.scrollTop = 0 } if (preserve_center && !(fits_width && fits_height)) { viewport.scrollLeft = Math.max(0, center_x * scale - viewport_width / 2) viewport.scrollTop = Math.max(0, center_y * scale - viewport_height / 2) } else if (scale <= min_scale + 0.02) { viewport.scrollLeft = 0 viewport.scrollTop = 0 } sync_buttons() } function fit_scale() { if (!natural_width) measure() const viewport_width = Math.max(viewport.clientWidth - 20, 1) const width_ratio = viewport_width / Math.max(natural_width, 1) fit_scale_value = clamp(width_ratio, 0.26, 1) min_scale = is_mobile() ? clamp(fit_scale_value * 0.7, 0.22, 0.46) : clamp(fit_scale_value * 0.68, 0.18, 0.92) max_scale = is_mobile() ? 1.6 : 1.9 const initial_scale = is_mobile() ? fit_scale_value : clamp(fit_scale_value * 0.97, min_scale, 1) apply_scale(initial_scale) } toolbar.addEventListener('click', (event) => { const button = event.target.closest('[data-mindmap-action]') if (!button) return const action = button.getAttribute('data-mindmap-action') if (action === 'zoom-in') apply_scale(scale + 0.12, { preserve_center: true }) if (action === 'zoom-out') apply_scale(scale - 0.12, { preserve_center: true }) if (action === 'reset') fit_scale() }) viewport.addEventListener('pointerdown', (event) => { if (is_mobile()) return if (event.pointerType === 'touch') event.preventDefault() viewport.setPointerCapture?.(event.pointerId) active_pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }) if (active_pointers.size === 1) { drag_start_x = event.clientX drag_start_y = event.clientY drag_start_left = viewport.scrollLeft drag_start_top = viewport.scrollTop viewport.classList.add('is-dragging') } else if (active_pointers.size === 2) { const points = [...active_pointers.values()] pinch_start_distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y) pinch_start_scale = scale viewport.classList.add('is-dragging') } }) viewport.addEventListener('pointermove', (event) => { if (is_mobile()) return if (!active_pointers.has(event.pointerId)) return if (event.pointerType === 'touch' && drag_enabled) event.preventDefault() active_pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }) if (active_pointers.size === 1) { if (!drag_enabled) return viewport.scrollLeft = drag_start_left - (event.clientX - drag_start_x) viewport.scrollTop = drag_start_top - (event.clientY - drag_start_y) return } if (active_pointers.size !== 2) return const points = [...active_pointers.values()] const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y) if (!pinch_start_distance) return apply_scale(pinch_start_scale * (distance / pinch_start_distance), { preserve_center: true }) }) function clear_pointer(event) { if (is_mobile()) return active_pointers.delete(event.pointerId) if (active_pointers.size < 2) { pinch_start_distance = 0 } if (active_pointers.size === 0) { drag_enabled = false viewport.classList.remove('is-dragging') } else if (active_pointers.size === 1) { const remaining = [...active_pointers.values()][0] drag_start_x = remaining.x drag_start_y = remaining.y drag_start_left = viewport.scrollLeft drag_start_top = viewport.scrollTop } } viewport.addEventListener('pointerup', clear_pointer) viewport.addEventListener('pointercancel', clear_pointer) viewport.addEventListener('pointerleave', clear_pointer) viewport.addEventListener('gesturestart', (event) => { if (!is_mobile()) event.preventDefault() }) viewport.addEventListener('gesturechange', (event) => { if (!is_mobile()) event.preventDefault() }) viewport.addEventListener('gestureend', (event) => { if (!is_mobile()) event.preventDefault() }) let resize_timer = null window.addEventListener('resize', () => { window.clearTimeout(resize_timer) resize_timer = window.setTimeout(() => { measure() fit_scale() }, 60) }) window.visualViewport?.addEventListener('resize', () => { window.clearTimeout(resize_timer) resize_timer = window.setTimeout(() => { measure() fit_scale() }, 60) }) measure() fit_scale() } })()