纳斯达克开盘钟声敲响那一刻,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()
}
})()