Two canonical roles
assets.icon is for compact, square placements. assets.logo is the wider wordmark for headers and cards.
Logo Yoink is a self-hosted API and CLI that discovers the real logo files a website exposes, validates them, and ranks the best icon and wordmark—without generating or inventing artwork.
assets.icon is for compact, square placements. assets.logo is the wider wordmark for headers and cards.
Every returned candidate includes its source, dimensions, role scores, variant metadata, and the reasons behind its score.
Ask for light or dark surfaces, color treatment, and transparent or opaque backgrounds. Matching is best-effort.
Quickstart
You need Node.js 22 or newer, npm, and internet access to inspect public websites. No database or API key is required.
$ git clone https://github.com/Hendrikc4/logo-yoink.git
$ cd logo-yoink
$ npm ci
$ npx playwright install chromium
$ cp .env.example .env.local
$ npm start
npm run smoke starts an isolated local server, verifies the homepage and API validation, then shuts it down. It does not contact a third-party site. For the full suite, run npm run check.
First request
Send JSON to the endpoint on the instance you control. The hosted endpoint powers the public demo and is intentionally rate-limited.
$ curl -sS http://127.0.0.1:4310/api/extract \
-H 'content-type: application/json' \
-d '{"website":"stripe.com"}'
const response = await fetch('http://127.0.0.1:4310/api/extract', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ website: 'stripe.com' }),
});
if (!response.ok) throw new Error(`Extraction failed: ${response.status}`);
const { assets, assetVariants } = await response.json();
console.log(assets.icon?.resolvedUrl);
console.log(assets.logo?.resolvedUrl);
/api/extractDiscovers, validates, deduplicates, and ranks logo assets for one public HTTP or HTTPS website.
websitestring · requiredA hostname or public HTTP(S) URL, up to 2,048 characters. Bare domains such as stripe.com are accepted.
preferencesobject · optionalRole-specific appearance preferences. Only icon and logo keys are accepted.
wikimediaFallbackboolean · optionalDefaults to true. Set to false to keep this request first-party-only.
Request options
Icon and logo preferences use the same fields. Omitted values normalize to any.
themeany | light | darkThe surface the asset will sit on. Use dark to prefer light artwork for a dark UI.
colorany | color | white | blackThe desired artwork treatment.
backgroundany | transparent | opaqueWhether the returned file should carry transparency.
{
"website": "stripe.com",
"preferences": {
"icon": { "color": "color" },
"logo": {
"theme": "dark",
"color": "white",
"background": "transparent"
}
}
}
Preferences are best-effort. A matching eligible asset ranks first when available; otherwise Logo Yoink returns the strongest eligible fallback.
Response
assetsThe canonical icon and logo. Start here for most integrations.
assetVariantsOrdered, semantically distinct treatments. The selected asset is first.
assetFamiliesRelated delivery-size copies grouped into artwork families.
candidatesEvery validated candidate with scores and evidence.
diagnosticsDiscovery counts, fallbacks, network work, and duration.
selectedByRoleCompatibility aliases: icon, wide, and legacy favicon.
{
"input": "stripe.com",
"domain": "stripe.com",
"homepage": "https://stripe.com/",
"preferences": {
"icon": { "theme": "any", "color": "any", "background": "any" },
"logo": { "theme": "dark", "color": "white", "background": "transparent" }
},
"assets": {
"icon": null,
"logo": {
"resolvedUrl": "https://…/logo.svg",
"dataUrl": "data:image/svg+xml;base64,…",
"format": "svg",
"width": 512,
"height": 170,
"source": "dom-img",
"variant": { "theme": "dark", "color": "white", "background": "transparent" },
"role_scores": { "icon": 28, "wide": 84, "favicon": 12 },
"predicted_roles": ["wide"],
"score_reasons": ["wide shape +30", "home linked +12"]
}
},
"assetVariants": {
"icon": [],
"logo": [{
"resolvedUrl": "https://…/logo.svg",
"certainty": { "score": 84, "band": "high" }
}]
},
"variantPolicy": { "minimumRoleScore": 45 },
"assetFamilies": [],
"candidates": [],
"diagnostics": { "discovered": 19, "validated": 8, "browserUsed": false, "durationMs": 1240 }
}
Errors & limits
400Invalid JSON fields, preference values, URL, or an extraction that could not complete.
403A cross-site browser request was rejected by the demo origin policy.
413The JSON request body is larger than the configured limit (2 KB by default).
415The request is not application/json.
429The per-client or global rate window is exhausted. Respect Retry-After.
503All extraction slots are busy. Retry after the provided delay.
Successful responses expose RateLimit-Limit and RateLimit-Remaining. Errors use the stable shape { "error": "Human-readable message." }.
CLI
$ npm run cli -- stripe.com
$ npm run cli -- stripe.com --theme dark --background transparent
$ npm run cli -- stripe.com --no-wikimedia-fallback
$ npm run cli -- stripe.com --role logo --download ./downloads/stripe--themeany | light | darkPreference for the logo destination surface.
--backgroundany | transparent | opaquePreference for the logo file background.
--roleicon | logoChoose which canonical winner to download. Icon-first is the default.
--downloaddirectoryWrite the selected validated asset to disk.
--deep-wideflagWhen needed, follow up to two strong first-party brand, press, or media links and inspect official ZIP kits.
--spa-bundlesflagWith deep discovery, inspect one same-origin entry bundle (up to 2.2 MB) for strong logo asset literals.
--no-wikimedia-fallbackflagDisable the default exact-domain Wikidata/Commons missing-role fallback.
Configuration
HOST127.0.0.1Local server bind address.
PORT4310Local server port.
BROWSER_DISCOVERY1Set to 0 to disable local Chromium discovery.
JINA_API_KEYunsetOptional recovery for blocked or unusable homepages.
BESTICON_URLunsetOptional URL for a self-hosted Besticon service.
PUBLIC_DEMO_ALLOW_JINA1Set to 0 to keep web/API requests off Jina.
PUBLIC_DEMO_BROWSER1Set to 0 to disable Chromium for web/API requests.
PUBLIC_DEMO_WIKIMEDIA1Set to 0 to disable exact-domain Wikidata/Commons recovery.
DEMO_RATE_LIMIT20 / 10 minPer-client in-process request window.
DEMO_GLOBAL_RATE_LIMIT60 / minPer-instance global request window.
DEMO_MAX_CONCURRENT2Simultaneous extraction ceiling.
Logo Yoink loads .env.local automatically. Keep secrets there; it is gitignored. The checked-in .env.example contains safe local defaults.
Deployment
The repository includes a Vercel function adapter and static configuration. The same app can run as a long-lived Node process with npm start.
Set HOST=0.0.0.0 in a container or VM, expose PORT, install Chromium, and run npm start.
Import the repository. vercel.json serves the static site, maps the API function, includes serverless Chromium, and applies security headers.
Under the hood
HTML images, picture sources, safe inline SVGs, schema data, metadata, manifests, touch icons, mask icons, tiles, and favicons.
Fetch under byte and time budgets, verify actual image bytes, measure dimensions and visible content, then reject unsafe or unusable files.
Deduplicate by URL and byte hash, then group delivery-size variations of the same artwork into asset families.
Score source quality, shape, size, placement, home-link evidence, company agreement, visual treatment, and negative context separately for icon and wordmark.
Bounded browser, deep first-party, SPA-bundle, cached favicon, Besticon, optional Jina, or exact-domain Wikidata/Commons recovery can fill a missing role.
Ranking
No model is called during extraction. AI-assisted review was used offline to label benchmark candidates and evaluate changes; the live ranker is an interpretable ruleset.
Minimum role score for an additional semantic variant.
Threshold for the high confidence band.
Frozen candidate judgments used to measure identity, role, and usability.
The ranker deliberately abstains when evidence is weak. A null canonical role is safer than silently returning a partner mark, product icon, UI glyph, or stale identity.
Security
Public targets only. Private, loopback, and reserved network destinations are rejected, including after redirects.
Bounded network work. HTML, images, bundles, redirects, time, and candidate counts have explicit ceilings.
Validated content. A response claiming to be an image is not trusted until its bytes pass format and renderability checks.
Hardened demo route. JSON-only input, origin checks, rate limits, coalescing, concurrency control, generic errors, and restrictive browser headers.
Limitations
Use the canonical picks for the common path, but preserve the candidate evidence and variants when human review matters.
Ready to ride?