{"schema_version":"verify.report.v1","generated_at":"2026-09-29T00:41:39.062152+00:00","snapshot_id":"trustsnap_d483a434f7295804","server":{"namespace":"awesome-oscarleoo","name":"chartlink-agents","title":"oscarleoo/chartlink-agents","description":"oscarleoo/chartlink-agents oscarleoo/chartlink-agents](https://glama.ai/mcp/servers/oscarleoo/chartlink-agents) 🎖️ 📇 ☁️ 🍎 🪟 🐧 - Charts, maps and sortable tables with live-updating embed links: draft, preview and publish, then update the data and every embed follows.","homepage_url":"https://github.com/oscarleoo/chartlink-agents","docs_url":"https://github.com/oscarleoo/chartlink-agents","icon_url":null,"support_url":"https://github.com/oscarleoo/chartlink-agents","remote_url":"https://chartlink.app/mcp","server_card_url":null,"latest_version":null,"current_status":"healthy","current_score":61.98,"transport_type":"streamable-http","has_oauth":false,"has_dcr":false,"has_prompts":true,"tool_count":10,"current_validation_schema_version":"8058defc70ca932c","last_validated_at":"2026-09-28T16:15:12.257869+00:00","registry_source":"awesome_mcp_servers","registry_identifier":"awesome_mcp_servers:oscarleoo/chartlink-agents","canonical_identifier":null,"current_score_components":{"auth_operability_score":2.0,"error_contract_score":1.0,"rate_limit_semantics_score":2.0,"schema_completeness_score":3.0,"backward_compatibility_score":4.0,"slo_health_score":4.0,"security_hygiene_score":3.0,"task_success_score":4.0,"trust_confidence_score":3.0,"abuse_noise_ratio_score":4.0,"prompt_contract_score":3.0,"resource_contract_score":4.0,"discovery_metadata_score":2.0,"registry_consistency_score":2.0,"installability_score":4.0,"session_semantics_score":4.0,"tool_surface_design_score":3.0,"result_shape_stability_score":3.0,"oauth_interop_score":0.0,"recovery_semantics_score":1.0,"maintenance_signal_score":3.0,"adoption_signal_score":2.0,"freshness_confidence_score":4.0,"transport_fidelity_score":4.0,"spec_recency_score":2.0,"session_resume_score":3.0,"step_up_auth_score":0.0,"transport_compliance_score":2.0,"utility_coverage_score":0.0,"advanced_capability_coverage_score":1.0,"connector_publishability_score":1.0,"tool_snapshot_churn_score":4.0,"connector_replay_score":4.0,"request_association_score":0.0,"interactive_flow_safety_score":4.0,"action_safety_score":1.0,"official_registry_presence_score":2.0,"safety_transparency_score":2.0,"tool_capability_clarity_score":4.0,"destructive_operation_safety_score":1.0,"egress_ssrf_resilience_score":3.0,"execution_sandbox_safety_score":2.0,"data_exfiltration_resilience_score":3.0,"least_privilege_scope_score":1.0,"secret_handling_hygiene_score":3.0,"dependency_supply_chain_signal_score":0.0,"input_sanitization_safety_score":3.0,"tool_namespace_clarity_score":4.0},"capability_taxonomy":[],"machine_summary":{},"taxonomy_tags":[],"score_decomposition":[],"validation_diff":null,"tool_snapshot_diff":null,"connector_replay":{},"request_association":{},"production_readiness":{"code":"safe_for_evaluation","label":"Safe for evaluation","reason":"The server is suitable for evaluation, but remaining gaps should be resolved before broad production use.","badge":"score-medium","critical_alerts":0,"high_or_critical_alerts":0,"degraded_by_active_alerts":false},"recommended_for":[],"history_summary":{},"validation_timeline":[],"evidence_confidence":{"score":95.0,"label":"high","reason":"Based on 16 recent validations, 16 captured checks, and validation age of 8.4 hours.","live_check_count":16,"validation_age_hours":8.44,"basis":{"evidence_bearing_validations":16,"affirmative_live_check_count":16,"validation_age_hours":8.44,"freshness_threshold_hours":24}},"incident_feed":[],"disputes":[],"remediations":[],"client_remediation_modes":[],"client_profiles":[],"client_readiness_verdicts":[],"publishability_policy_profiles":[],"compatibility_fixtures":[],"install_snippets":{},"aliases":[],"raw_evidence":{"checks":{"probe_noise_resilience":{"status":"ok","latency_ms":135.73,"details":{"url":"https://chartlink.app/robots.txt","http_status":200,"headers":{"content-type":"text/plain; charset=utf-8","strict-transport-security":"max-age=63072000"},"validation_disallowed":false,"consent_error":null}},"server_card":{"status":"error","latency_ms":35.89,"details":{"url":"https://chartlink.app/.well-known/mcp/server-card.json","error":"Client error '404 Not Found' for url 'https://chartlink.app/.well-known/mcp/server-card.json'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404"}},"oauth_protected_resource":{"status":"error","latency_ms":72.69,"details":{"url":"https://chartlink.app/.well-known/oauth-protected-resource","error":"Client error '404 Not Found' for url 'https://chartlink.app/.well-known/oauth-protected-resource'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404"}},"oauth_authorization_server":{"status":"missing","latency_ms":null,"details":{"reason":"no_authorization_server"}},"openid_configuration":{"status":"missing","latency_ms":null,"details":{"reason":"no_authorization_server"}},"initialize":{"status":"ok","latency_ms":106.81,"details":{"url":"https://chartlink.app/mcp","payload":{"result":{"protocolVersion":"2025-03-26","capabilities":{"resources":{"listChanged":true},"prompts":{"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"chartlink","version":"0.1.0"},"instructions":"chartlink makes charts, maps and tables with live-updating embeds. No accounts: the API key is the workspace.\nTHE TEMPLATE LOOP (the default, four steps): list_templates {q: what the person asked for} → get_template {id} (its contract:\nknobs, columns, region codes, an example call) → create_from_template {template, data, knobs, publish: true} → hand back\nurls.embedIframe / urls.png / urls.page. Knobs are the template's few settings (theme, dataType, texts, scale); the design\nis the template's. No key on this connection? Call signup. The full engine (every chart type, the whole settings schema)\nis the advanced path: connect to /mcp?full=1 or read https://chartlink.app/llms-full.txt.\n\nThe engine loop, for the advanced path: list_asset_types → get_spec_schema(type) once (read its notes, examples and defaults, not just the schema) → create_asset {type, data, config} (a 201 means it validated AND rendered;\nthe preview image comes back inline) → update_asset with a small configPatch → publish_asset → hand back urls.embedIframe / urls.png / urls.page.\nNew numbers later: replace_asset_data (republishes every embed). A CSV/JSON URL that refreshes itself: set_data_source.\nPrefer a template: list_templates {q: what the person asked for} ranks them by need — take the first, then create_asset {template, data}. Maps: list_geographies before naming regions.\nHumans fine-tune with create_edit_link (publish first — the editor cannot publish a draft), so keep configs lean:\none element per idea (a multi-colour headline is ONE title with [spans](#hex) and \n, not stacked texts[]; a grid is\nyAxis.gridlines, not reference lines), and 2–3 decimals for position fractions.\nThe inline preview is 640px: before judging a hairline, a gridline or a label, fetch /api/assets/{id}/preview.png?width=1600.\nNo key on this connection? Call signup. Manual: https://chartlink.app/llms.txt"},"jsonrpc":"2.0","id":1},"http_status":200,"headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"}}},"protocol_version_probe":{"status":"warning","latency_ms":null,"details":{"claimed_version":"2025-03-26","validator_protocol_version":"2025-03-26","latest_known_version":"2025-11-25","releases_behind":2,"lag_days":244}},"tools_list":{"status":"ok","latency_ms":71.88,"details":{"url":"https://chartlink.app/mcp","payload":{"result":{"tools":[{"name":"signup","title":"Get an API key — no account, one call","description":"Creates a workspace and returns its API key (shown ONCE — keep it). Works on a keyless connection; every other tool needs the key. Then put the key where your client keeps it (plugin api_key setting, the Authorization header, or CHARTLINK_API_KEY for the npx bridge) and reconnect.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"description":"A name for the workspace (the project, the newsletter…)","type":"string"},"ref":{"description":"Where you heard of chartlink — free text","type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"whoami","title":"Verify your API key","description":"Returns key name + workspace. Call this first — non-destructive auth check. Everything you write is stamped createdBy/updatedBy = key name.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{}},"annotations":{"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"list_templates","title":"Templates by need — search, then start a chart from the best match","description":"Templates are named by the NEED they answer (need, asks, tags), each with urls.png (show it to your human) and `columns`: the column ids your data must supply. Pass q with what the person asked for — \"map of brazil's states\", \"ranked bars with flags\", \"change between two years\" — and the list comes back ranked; take the first. Then create_asset with template: <id> and your data — the whole design comes along. Any chart id you were shown works as a template too, not only these.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"q":{"description":"What is needed, in the person's words","type":"string"},"type":{"description":"Only this asset type, e.g. \"bar\"","type":"string"},"tags":{"description":"Comma-separated tags every result must carry, e.g. map,united-states","type":"string"},"limit":{"description":"At most this many, best first (default 20)","type":"integer","minimum":1,"maximum":200}}},"annotations":{"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"get_template","title":"One template's contract: knobs, columns, region codes, an example call","description":"Read this before create_from_template. Returns the template with `contract`: its looks (theme × dataType × surroundings), the knobs JSON Schema, the columns with their roles, the region-code convention with three real codes and a lookup URL, what each dataType expects of the value column, and a complete example call whose shape you copy. Takes an id, the page slug, or the page address as on chartlink.app/maps — sweden/municipalities, us/texas/counties, london/wards — so you can skip list_templates when you know the place.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","description":"Template id (ast_…), slug, or page address (place/level)"}},"required":["id"]},"annotations":{"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"check_template_data","title":"Dry run: would this data and these knobs work with the template? Creates nothing","description":"Matches every region value against the template's geography and reports unmatched values with the closest real codes, how many regions would stay grey, and the scale the map would use. No key needed. Use it when unsure of the codes; then create_from_template with the same data and knobs.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"template":{"type":"string","description":"Template id, slug, or page address (sweden/municipalities)"},"data":{"type":"object","properties":{"columns":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Column identifier referenced by encodings"},"type":{"type":"string","enum":["string","number","date","boolean"],"description":"date columns parse ISO-ish strings on their own: \"2024\", \"2024-03\", \"2024-03-15\", \"2024-Q2\", full ISO timestamps, or epoch numbers. Anything else needs dateFormat."},"label":{"description":"Display label; defaults to id","type":"string"},"dateFormat":{"description":"For date columns whose strings are NOT ISO-ish: a strftime-style parse hint tried first — tokens %Y %m %d %b %B %q %H %M %S, e.g. \"%d/%m/%Y\" for \"15/03/2024\", \"%b %Y\" for \"Mar 2024\". Removes the day/month guess; ISO-ish strings never need it.","type":"string"}},"required":["id","type"],"additionalProperties":false}},"rows":{"type":"array","items":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]}},"description":"Row-major values, one array per row, order matching columns"}},"required":["columns","rows"],"additionalProperties":false,"description":"The data you intend to send"},"knobs":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["template","data"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"create_from_template","title":"Create a chart from a template with your data and a few knobs — returns an inline preview","description":"The template's design, your data, your words. `knobs` are the template's settings (get_template shows the schema): theme light|dark|editorial, dataType gradient|buckets|categories and surroundings true|false (then waterColor, landColor) for maps, title/description/source/brand {text, fontFamily, fontSize, color}, backgroundColor, and the scale knobs for the dataType (colormap and direction, range, breaks or classes, categoryColors/categoryOrder). data = {columns: [{id, type}], rows: [[…]]}: use the template's column ids, or exactly two columns (region first, value second) for a map. publish: true makes the URLs live in the same call. A success means it validated AND rendered — look at the preview. Region values that match nothing come back as a 400 listing each with its closest real codes, before anything is made — no separate check needed. `config` is the advanced path: the engine's settings, merged after the knobs.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"template":{"type":"string","description":"Template id or slug from list_templates, or the page address (sweden/municipalities, us/texas/counties, london/wards) when you know the place"},"data":{"type":"object","properties":{"columns":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Column identifier referenced by encodings"},"type":{"type":"string","enum":["string","number","date","boolean"],"description":"date columns parse ISO-ish strings on their own: \"2024\", \"2024-03\", \"2024-03-15\", \"2024-Q2\", full ISO timestamps, or epoch numbers. Anything else needs dateFormat."},"label":{"description":"Display label; defaults to id","type":"string"},"dateFormat":{"description":"For date columns whose strings are NOT ISO-ish: a strftime-style parse hint tried first — tokens %Y %m %d %b %B %q %H %M %S, e.g. \"%d/%m/%Y\" for \"15/03/2024\", \"%b %Y\" for \"Mar 2024\". Removes the day/month guess; ISO-ish strings never need it.","type":"string"}},"required":["id","type"],"additionalProperties":false}},"rows":{"type":"array","items":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]}},"description":"Row-major values, one array per row, order matching columns"}},"required":["columns","rows"],"additionalProperties":false,"description":"Typed columns + row-major rows, shaped like the template's example"},"knobs":{"description":"The template's knobs — get_template returns their schema","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"publish":{"description":"true = published and live in this call; omit while you iterate","type":"boolean"},"config":{"description":"ADVANCED: engine settings deep-merged after the knobs","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["template","data"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"update_asset","title":"Update a draft (deep-merge patch, or replace the config) — returns an inline preview image","description":"configPatch is DEEP-MERGED: objects merge recursively, arrays replace wholesale, null clears a key — send only what changes. To REMOVE keys (drop document.aspect, delete one of texts[]) pass `config` instead: it REPLACES the whole config (get_asset returns the current one to edit). `data` (if given) replaces rows wholesale. Requires expectedVersion from the last envelope; on 409 re-merge onto the returned `current`. Updating does NOT publish by default — pass publish: true to publish in the same call, or call publish_asset separately.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string"},"expectedVersion":{"type":"integer","minimum":1,"maximum":9007199254740991},"publish":{"description":"true = publish immediately after this update succeeds (one call instead of two)","type":"boolean"},"configPatch":{"description":"Deep-merged onto the current config","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"config":{"description":"REPLACES the whole config — the way to remove keys; applied before configPatch if both are given","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"data":{"type":"object","properties":{"columns":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Column identifier referenced by encodings"},"type":{"type":"string","enum":["string","number","date","boolean"],"description":"date columns parse ISO-ish strings on their own: \"2024\", \"2024-03\", \"2024-03-15\", \"2024-Q2\", full ISO timestamps, or epoch numbers. Anything else needs dateFormat."},"label":{"description":"Display label; defaults to id","type":"string"},"dateFormat":{"description":"For date columns whose strings are NOT ISO-ish: a strftime-style parse hint tried first — tokens %Y %m %d %b %B %q %H %M %S, e.g. \"%d/%m/%Y\" for \"15/03/2024\", \"%b %Y\" for \"Mar 2024\". Removes the day/month guess; ISO-ish strings never need it.","type":"string"}},"required":["id","type"],"additionalProperties":false}},"rows":{"type":"array","items":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]}},"description":"Row-major values, one array per row, order matching columns"}},"required":["columns","rows"],"additionalProperties":false,"description":"Typed columns + row-major rows. A date column whose strings are not ISO-ish takes dateFormat (e.g. \"%d/%m/%Y\")."},"title":{"type":"string"},"brand":{"anyOf":[{"type":"string"},{"type":"null"}]},"showcase":{"description":"Offer this chart as a template in the public gallery and list_templates (once published). Workspace keys only.","type":"boolean"}},"required":["id","expectedVersion"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"publish_asset","title":"Publish — snapshot a version and update every embed","description":"Makes the draft live. Returns urls: paste urls.embedIframe on iframe platforms; on Substack insert urls.png as an image and link it to urls.page. Pinned history: urls.png + '?v=N'.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string"},"note":{"type":"string"},"premium":{"description":"Spend one paid credit to publish as premium (2400px, SVG, your branding, no badge) in this call","type":"boolean"}},"required":["id"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"create_edit_link","title":"Mint a no-login tweak link — hand it to your human","description":"Returns a URL opening the visual tweak panel for ONE chart (text, colors, spacing, label nudges) with no login. PUBLISH FIRST: the editor saves and republishes a published chart, it cannot publish a draft. Scope: view, tweak, and republish that chart only — never delete, never spend credits, nothing else in the workspace. OFFER THIS whenever the human's request is aesthetic fine-tuning: handing over the wheel beats a nudge-by-nudge back-and-forth. The secret is shown once; links don't expire and can be revoked with revoke_edit_link.","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","description":"ast_..."}},"required":["id"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"submit_feedback","title":"Report a gap — don't silently give up","description":"If a capability is missing, a schema fights you, or docs are wrong: record it here so the owner can fix it. One report per distinct issue, with a real description — reports get fixed same-day when they say what you tried, what you expected, and what happened (title-only reports can't be acted on).","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"type":{"type":"string","enum":["bug","feature_request","limitation","documentation_gap","other"]},"title":{"type":"string"},"description":{"type":"string","minLength":40,"description":"What you tried, what you expected, what happened instead — include the asset id if one is involved"},"assetId":{"type":"string"}},"required":["type","title","description"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}}]},"jsonrpc":"2.0","id":2},"http_status":200,"headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"}}},"prompts_list":{"status":"ok","latency_ms":61.81,"details":{"url":"https://chartlink.app/mcp","payload":{"result":{"prompts":[{"name":"make-a-chart","title":"Make a chart from data","description":"The chartlink loop for one chart: find the template by need, read its contract, create from it with your data and knobs, hand back the embed.","arguments":[{"name":"brief","description":"What the chart should say, and where the data is","required":true}]}]},"jsonrpc":"2.0","id":3},"http_status":200,"headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"}}},"prompt_get":{"status":"error","latency_ms":75.7,"details":{"url":"https://chartlink.app/mcp","payload":{"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"MCP error -32602: Invalid arguments for prompt make-a-chart: [\n  {\n    \"expected\": \"string\",\n    \"code\": \"invalid_type\",\n    \"path\": [\n      \"brief\"\n    ],\n    \"message\": \"Invalid input: expected string, received undefined\"\n  }\n]"}},"http_status":200,"headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"},"prompt_name":"make-a-chart","prompt_arguments":[{"name":"brief","description":"What the chart should say, and where the data is","required":true}]}},"resources_list":{"status":"ok","latency_ms":50.83,"details":{"url":"https://chartlink.app/mcp","payload":{"result":{"resources":[{"uri":"chartlink://llms.txt","name":"manual","title":"chartlink manual (llms.txt)","description":"The complete agent manual: settings shape, chart types, the create → preview → publish loop, errors.","mimeType":"text/markdown"}]},"jsonrpc":"2.0","id":5},"http_status":200,"headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"}}},"resource_read":{"status":"ok","latency_ms":55.4,"details":{"url":"https://chartlink.app/mcp","payload":{"result":{"contents":[{"uri":"chartlink://llms.txt","mimeType":"text/markdown","text":"chartlink — an agent-first platform for charts, maps and tables with live-updating embed links.\n\nNO API KEY YET?\nPOST /api/signup (optional body {\"name\": \"my project\", \"ref\": \"where you heard of chartlink\"}) returns a\nworkspace and its API key in one unauthenticated call — instantly usable. The key is shown EXACTLY ONCE: store it. There are no\naccounts, logins or sessions anywhere: the key is the workspace, an edit link is one chart, and a\nshare URL is one published chart. Key rotation is POST /api/keys + DELETE /api/keys/{id}; credits\nare GET /api/billing-info and a checkout link from POST /api/checkout-link, which you hand to a human.\n\nWHAT AN ASSET IS\nCHART TYPES: line, bar, area, scatter, dumbbell, slope, heatmap, choropleth, symbol-map, table, pie, waterfall\n(GET /api/asset-types describes each; GET /api/asset-types/{type}/schema is the contract — read it before the first create.)\nAn asset = type + data + config (+ brand). data holds typed columns and row-arrays; config holds\neverything about presentation (title and the other text elements, encoding, chart options). They are updated independently:\nPUT /assets/{id}/data replaces the numbers; PATCH /assets/{id} deep-merges config. Never resend\nwhat you are not changing. Tables (type \"table\") are assets too: embeds render sortable HTML, while\nimage surfaces (.png, e.g. Substack) snapshot the first chart.maxRowsPng rows (default 15).\nTables never truncate: every column claims exactly the width its content needs and every cell\nrenders whole. A wide table simply renders wider, and every surface (PNG, embed, share page) shows\nthe identical picture scaled to fit — so text length is YOUR editorial call: short labels and\ncountry codes keep tables crisp; look at the preview and judge.\n\nTHE ITERATION LOOP\n1. POST /assets — create a draft. The response includes urls.previewPng (renders the CURRENT draft,\n   never cached; requires the same Authorization header as the API — it is NOT a public URL).\n   A 200/201 means the config validated AND rendered. Public URLs (urls.png, urls.page,\n   urls.embedUrl) serve only the published snapshot.\n2. Look at the preview. PATCH /assets/{id} with a minimal configPatch to adjust. Deep-merge rules:\n   objects merge recursively, arrays replace wholesale, explicit null clears a key. To REMOVE keys\n   wholesale send \"config\" instead of configPatch — it replaces the entire config.\n3. POST /assets/{id}/publish — snapshots an immutable version and flips every embed/PNG to it.\n   Pass {\"premium\": true} to spend one credit at publish and pin the premium tier in the same call.\n   (Shortcut: POST /assets with publish: true does steps 1 and 3 in one call, for a chart that is\n   already final. Skip it while you are still looking at previews.)\n4. Paste urls.embedIframe (iframe platforms) or urls.png + urls.page (Substack: insert the PNG as an\n   image, link it to the page). urls.csv serves the published data publicly (data transparency —\n   readers can cite your chart), and /a/{id}.json serves the published type + config + columns, so\n   ANY agent can remix a chart it was shown: POST /assets with that type and config and its own data.\n   URLs come from the response — never construct them yourself.\n\nDATA UPDATES (the core flow)\nPUT /assets/{id}/data?publish=auto replaces rows. If the asset is published, it republishes\nautomatically — every embed, PNG, and the share page serve the new numbers immediately.\npublish=false stages the change on the draft instead; publish=true force-publishes a draft.\nThe new data is checked against the chart's current config: a column the encoding names but the data\nlacks is a 400 naming the columns. When the columns CHANGE (a rename, a number column becoming a class\ncolumn), send the new settings in the same call — {\"data\": …, \"config\": …} — so both are checked as a pair.\nThe body wraps the payload in a \"data\" key — a bare {columns, rows} object is rejected:\n  {\"data\": {\"columns\": [{\"id\": \"year\", \"type\": \"date\"}, {\"id\": \"value\", \"type\": \"number\"}],\n            \"rows\": [[\"2024\", 1.2], [\"2025\", 1.4]]}}\nDate columns parse \"2024\", \"2024-03\", \"2024-03-15\", \"2024-Q2\", full ISO timestamps and epoch numbers\non their own. For anything else give the column a dateFormat (strftime tokens %Y %m %d %b %B %q %H %M %S):\n{\"id\": \"day\", \"type\": \"date\", \"dateFormat\": \"%d/%m/%Y\"} — it is tried first, so day/month order is never guessed.\n\nSELF-UPDATING CHARTS (data sources)\nPUT /assets/{id}/source {\"url\": \"...\", \"refresh\": \"hourly\"|\"daily\"|\"weekly\", \"format\": \"auto\"|\"csv\"|\"json\"}\nattaches a public URL the platform refetches on schedule: rows are replaced from it and a published\nasset republishes itself after every successful fetch — no agent in the loop, and never metered.\nThe asset's columns stay authoritative: CSV headers / JSON object keys must match column ids or\nlabels (JSON also accepts {\"rows\": [[...]]} positionally). Setting the source fetches ONCE\nimmediately and returns the outcome — check fetch.ok and fix fetch.error before moving on. The\nlast outcome is always visible on GET /assets/{id} as dataSourceState. POST /assets/{id}/source/fetch\nrefreshes now; DELETE /assets/{id}/source detaches (data stays).\nThis is also the BULK IMPORT path: for large datasets, don't paste thousands of rows through a tool\ncall — put the CSV at a URL and attach it as the source (one fetch fills the asset; cap 2MB of\ndata). Direct PUT /assets/{id}/data accepts the same 2MB over plain HTTP.\n\nDRAFT vs PUBLISHED\nEmbeds always serve the last published snapshot. Draft edits are invisible to readers until publish.\nPinned URLs (/a/{id}.png?v=3) are immutable forever. Unpublish → embeds return 410.\nPropagation after (re)publish: the share page and embed update on the next request; the bare\n.png/.svg URLs sit behind the CDN and can serve the previous render for up to ~2 minutes. Need\nthe new render guaranteed? Use the pinned ?v=N URL from the publish response.\n\nCONCURRENCY\nMutations take expectedVersion. On 409 you get {currentVersion, current}: re-merge your intent onto\ncurrent and retry with the new expectedVersion. Do not blindly retry. PUT data may omit\nexpectedVersion (wholesale refresh semantics).\n\nMAPS (choropleth geographies)\nA map colours REGIONS, and regions are data: chart.geography names a boundary set, and the region\ncolumn names regions in it. Built in: \"countries\" (ISO alpha-2/alpha-3 or names), \"us-states\"\n(USPS codes; Alaska and Hawaii as insets) and \"us-states-pr\" (the same plus Puerto Rico as a third\ninset). Every country's states/provinces exist as \"<alpha-2>-regions\" — de-regions, ru-regions,\nbr-regions, in-regions… — with regions named by ISO 3166-2 code (DE-BY), the bare suffix (BY), the\nEnglish or local name. Below the US states: \"us-counties\" (3,220 counties and Puerto Rico's municipios,\n5-digit FIPS codes or \"Name, ST\"; same frame as us-states, so chart.borders {geography: \"us-states-pr\"}\ndraws the state lines on top), \"us-districts\" (congressional districts of the 119th Congress, codes\n\"CA-05\", at-large \"AK-AL\"), \"us-metros\" (393 metropolitan statistical areas, 5-digit CBSA codes or the\nCensus title), \"us-zip3\" (894 three-digit ZIP areas, codes \"902\") — all in the states' frame and\ninsets, so chart.borders {geography: \"us-states-pr\"} lines up — and one lon/lat set of 5-digit ZIP\ncodes per state: \"us-zips-ca\", \"us-zips-tx\", \"us-zips-ny\"… (ZCTA codes). GET /api/geographies lists every geography; GET /api/geographies/{id} lists its\nregions with codes, names and aliases — read it before naming regions. An unknown region fails loudly\nand names the closest matches; an unknown geography lists the valid ids. chart.bounds crops any\nlon/lat geography; a sub-national geography fits its own extent when bounds is omitted. Boundaries\ncome from Natural Earth (public domain).\nchart.projection picks how the sphere is flattened. \"auto\" (default) is equal-area azimuthal for a\nregional crop and Natural Earth for the world. For a world data map prefer \"equalEarth\" or\n\"naturalEarth\" — they keep areas honest. \"mercator\" is what readers expect for a country or city\ncrop (poles clamped at ±85°). \"conicConformal\" is the atlas standard for a mid-latitude continent or\ncountry (Europe, the US). \"orthographic\" is a globe facing the crop's centre — bounds pick the\nhemisphere it shows and may span at most 180°. \"us-states\" is pre-projected (Albers USA) and ignores\nprojection.\nPOINTS on maps, two ways. (1) The \"symbol-map\" type: rows ARE points — encoding {lon, lat, size?,\ncolor?, label?} puts a circle at each row's coordinates on a grey basemap, area ∝ the size column\n(radius ∝ √value), one palette colour per category of a STRING color column with a swatch legend,\nlabels \"none\" | \"all\" | [names]. It takes the same chart.geography / bounds / projection as the\nchoropleth. Points outside the crop are not drawn (all of them outside is an error); a size column gets a\nthree-circle size legend (chart.sizeLegend).\nyet. (2) chart.pins on either map type: config-level pins [{lon, lat, label?, color?, size?}] for a\ncapital, an event, a headquarters — a pin outside the drawn map fails loudly. Coordinates are lon\nTHEN lat, the GeoJSON order; swapped columns fail with a range error.\n\nTEMPLATES (the fastest way to a chart that looks right)\nSEARCH FIRST: GET /api/templates?q=<what the person asked for> (\"map of brazil's states\", \"ranked bars with\nflags\", \"change between two years\") ranks templates by the need they answer — take the first. Each\ntemplate has a page at /templates/{slug} with the need, the preview, the columns and the recipe.\nA template may carry variants: the same design in other looks, keyed \"dark\" (a dark canvas), \"categories\"\n(a map with a text category per region), \"buckets\" (values in classes) or a combination like\n\"categories-dark\" — hidden charts, not listed on their own; pass the variant's id as the template for that look.\nAny published chart is a template: POST /assets with {\"template\": \"<asset id>\", \"data\": {...}, \"config\":\n{\"title\": {\"text\": \"...\"}}} copies its type and whole design and merges your config on top. Your data\nmust supply the column ids the template's encoding expects — GET /api/templates/{id} lists them, and a\nmismatch is a 400 that names them. GET /api/templates lists charts their owners offered as starting\npoints, each with urls.png: show a human three or four and ask which; or pick yourself. Humans browse\nthe same list at /templates and hand you a chart id. Prefer a template over designing from a blank\nconfig: adapting a proven design beats inventing one. Offer your own chart as a template with PATCH\n{\"showcase\": true}. Remixing a chart you were merely shown works the same way — /a/{id}.json is its\nrecipe.\n\nHAND-TUNING BY HUMANS (edit links)\nPOST /assets/{id}/edit-link mints a no-login URL that opens a visual tweak panel for that ONE\nchart (text, colors, spacing, label nudges — same config you author, saved through the same API).\nPUBLISH FIRST: the editor saves and republishes a published chart; it cannot publish a draft.\nOffer it whenever the human's request is aesthetic fine-tuning; handing over the wheel beats a\nnudge-by-nudge loop. Scope if leaked: restyle/republish that one chart — never delete, never\ncredits, nothing else. Revocable (DELETE /assets/{id}/edit-links/{tokenId}); edits stamp\nupdatedBy \"edit-link\" so you can see when your human changed something.\n\nRENDERING SEMANTICS (the conventions you can't guess from schemas)\nSizing: OMIT document.aspect and the canvas adapts to its content — the text elements take the\nspace they need (a long description grows the canvas, never squeezes the plot), the plot keeps its\nnatural size, row-driven types (dumbbell, heatmap, horizontal bars, table) grow with their rows, and\nchoropleth derives height from its bounds. Set document.aspect only when the frame must be a fixed\nshape (a 1:1 social card, a 3:4 print slot). chart.bounds is a crop,\nand when document.aspect doesn't match its shape, more map shows on the letterboxed axis — omit\naspect for the tightest crop.\nLayout is a document: the canvas takes document.box.padding, then the elements — title, description,\nlegend, notes, source, chart — stack in that order, each inside its own box. There is no gap\nvocabulary: the space between two elements is the sum of the box.margins that touch. The chart takes\nwhatever the text leaves, so chart.box.margin is measured from the document's padding box — 0 spans\nit exactly, and NEGATIVE values bleed outward past it toward the canvas edge.\nFull-bleed (edge-to-edge maps): set document.box.padding to 0 and give the text elements margins —\n{document: {box: {padding: {top: 0, right: 0, bottom: 0, left: 0}}},\n title: {box: {margin: {left: 26, top: 26}}}, source: {box: {margin: {left: 26, bottom: 14}}}}. The\nchart then spans the canvas at its default 0 margin and the text keeps its inset. Prefer this over a\nnegative chart margin: negative values are absolute, so they only reach the edge while the document\npadding happens to match them.\nEscaping the flow: give any element a position {space, x, y, anchor, verticalAnchor, offset} — x/y are\nfractions of the canvas (space \"canvas\", the default) or of the plot area (space \"plot\"), required\ntogether; anchor says which END of the block sits at x, verticalAnchor which edge sits at y; offset\nnudges in canvas units afterwards. The element is lifted out of the stack to that spot and the chart\nreclaims the room it would have taken. Free text works the same way: texts[] is an array of\n{text, font, align, box, position} drawn wherever you like, independent of title/description (set\ntitle.text and description.text to \"\" and caption purely with texts[]).\nThe badge too: badge.position lifts it out of the footer row, so a full-bleed map with no source\nreaches the bottom edge and the badge sits over it in a corner, e.g.\nbadge: {position: {x: 0.98, y: 0.97, anchor: \"right\", verticalAnchor: \"bottom\"}, font: {color: \"#fff\"}}.\nWithout a position the badge shares the source line's row, and reserves it even when the source is off.\nFonts inherit: a free text starts from the description's font, notes and the badge from the source's,\nan annotation from chart.annotationStyle.font; font{} overrides key by key.\nAnything positioned — an element or free text — renders ABOVE the chart, so a title can sit on top\nof a full-bleed map.\nZ-order: highlights and reference lines sit behind the data unless layer: \"front\"; annotations render\nabove it; on bar charts the zero baseline draws over the bars so it never disappears; positioned\nelements draw above everything.\nText over a busy fill: give the font an outline {width, color} and the glyphs get a stroke\n(paint-order stroke). This is EXPLICIT — the engine never adds a knockout halo behind map labels on a\nluminance guess. Map labels default to regionLabels.color \"auto\", which contrast-picks per region and\nneeds no outline; a forced regionLabels.color with an outline on regionLabels.font is how you keep it\nreadable. An 8-digit hex carries opacity (#161a22cc).\nA box behind anything: every element's box takes background, border {width, color, dash}, radius and\npadding — a title on a tinted band, a chart area with a hairline frame, an annotation as a callout\nbox. Set background or border and the box is drawn; otherwise only its spacing applies.\nLabels: things that don't fit are DROPPED, never squeezed — heatmap cell values, choropleth region\nlabels (small regions can get curated leader labels), scatter point labels (which also never cover\na dot). seriesLabels.show \"direct\" names every series; suppress one by giving it label: \"\". Legends appear\nwhen direct labels can't (grouped bars, seriesLabels.show \"legend\"), and their swatches match the mark: circles for\ndots, strokes for lines, squares for fills.\nColor ramps: palette.diverging arrays read in ramp order — negative is extreme-first, positive is\nmidpoint-first; the diverging midpoint is pinned to value 0. chart.domain (heatmap, choropleth)\npins the scale; out-of-range values clamp to the ramp ends and the legend caps with ≤/≥.\nHover (line, area): chart.tooltip \"auto\" lists every series at the crosshair when there are 8 or fewer,\nelse only the line under the pointer plus emphasized series, brought forward; \"all\" / \"nearest\" /\n\"none\" force it. A series with label \"\" hides its direct label only — the tooltip names it anyway.\nAnnotations are text elements in data space: {text, position: {x, y, offset}, font, align, box,\nleader, marker}. position.x/y is the point (a category, date or number on the x axis, a value on the\nleft axis; space \"canvas\"/\"plot\" places by fraction instead); offset {x, y} is where the label sits\nrelative to it. leader draws the line from the label to the point — true for the defaults, or\n{style: curve|straight|elbow, head: filled|open|none, from: left|right|top|bottom, curvature, stroke};\nmarker styles the dot. Several annotations that share a look set it ONCE in chart.annotationStyle\n{font, align, box, leader, marker} — merged under every entry, an entry's own keys win. An annotation\nwith box.background is a callout box; there is no separate info-box type.\nReference lines: {axis, value, layer, stroke {width, color, dash}, label {text, font, side}}; on a scatter\naxis \"diagonal\" draws y = x + value (value 0 = the identity line, the argument of any like-vs-like\nscatter). Highlights: {axis, from, to, layer, fill {color, alpha}, label {text, font}}.\nBands (line): chart.bands [{lower, upper, series, color, alpha, label}] fill between two edges behind the\nlines — each edge a column id or a number: a confidence interval around a series (lower \"p10\", upper\n\"p90\", series \"p50\" takes the line's colour), the range between two series, or a reference zone between\ntwo numbers. matplotlib's fill_between.\nScatter: chart.emphasize [names from the label column] colours those points with palette.emphasis and\nnames them; the rest fade to palette.deemphasis — the six-countries-called-out treatment;\nchart.emphasisStyle {fill, stroke, radius, font} restyles them. encoding.size {column} sizes dots by\narea (chart.symbol {minRadius, maxRadius, opacity}) with a three-circle size legend (chart.sizeLegend).\nDate x axes: xAxis.ticks.count sets the tick density (14 for every 5 years over 1960–2025); gridlines\nfollow the ticks.\nMaps: chart.context {geography, fill, stroke} draws a second geography beneath the map through the same\nprojection and crop — a country's regions inside the world's countries, so a region map does not float;\non a pre-projected US map it takes a same-frame geography (us-states-pr beneath us-metros: faint state\nlines under the metros, which stay whole where they straddle a state line).\nchart.borders {geography, width, color} draws a coarser geography's outlines ON TOP, unfilled — state\nborders over us-counties or us-zip3 (geography \"us-states-pr\"), country borders over a lon/lat region map. borders.only [codes or names] keeps just those regions of it — one state's outline over its ZIP codes\n({geography: \"us-states-outline\", only: [\"TX\"]}; us-states-outline is the lon/lat outline of every state).\nchart.outline {width, color} draws the region outlines (default a 0.75 hairline in the document\nbackground; 0 removes them). preset: \"map-light\" | \"map-dark\" | \"map-editorial\" (+ \"-surroundings\") names the engine's own map design — padding,\nkey spacing, hatching, edge, inset, colours, fonts — merged BENEATH the config at every render, so the design\nevolves with the engine; any key you set overrides it. The map templates carry one; a chart made from a\ntemplate keeps it. chart.missing {style \"plain\"|\"hatch\", color} draws the regions with no data row as a flat half-strength\ngrey or as thin diagonal hatching (the map templates hatch). chart.inset (canvas units) keeps the drawn geography that much inside the map area when nothing\ncrops it — the water and surroundings still fill the area. chart.edge {width, color} draws a thicker line around the OUTSIDE of\neverything drawn — the state around its counties, the country around its municipalities — while the\nlines between regions stay hairline (default none; the map templates draw it at 1 in the line colour). chart.colorBar (maps and heatmaps) is the colour legend: style \"gradient\"\n(the bar; radius rounds its corners) or \"dots\" (a row of sample swatches, steps of them, each with its\nvalue underneath — 3 reads as low / middle / high); align left/center/right; position bottom (under the\nmap), top (between the description and the map), or {x, y, anchor, verticalAnchor} to draw it OVER the\nmap (the map then takes the whole plot — {x: 0.97, y: 0.95, anchor: \"right\", verticalAnchor: \"bottom\"}\nis the bottom-right corner); width \"auto\" | \"full\" | px; labelPosition below | sides. The top-level\nlegend element is the SERIES KEY (class maps, lines, bars); a numeric map's legend is chart.colorBar.\nA map whose value column is TEXT is a class map: each region takes its class's colour from\npalette.categorical (first-appearance order — sort the rows to order the key) and the key is the\ntop-level legend element: legend.direction \"column\" stacks one class per line; legend.position\n{space: \"canvas\", x: 0.97, y: 0.3, anchor: \"right\", verticalAnchor: \"top\"} puts it beside the map\n(give chart.box.margin.right room for it); legend.box {background, border, padding, radius} frames it. A projection or bounds on the pre-projected us-states is an error, and a\nregionColors key that names no region of the geography is an error with the near misses.\nDumbbell: encoding.points [{column, label, color, labelPosition}] takes two OR MORE value columns per row\n(2000 · 2012 · 2024); the connector runs from the leftmost dot to the rightmost, the last point takes\nthe palette's first colour, and value labels (valueLabels.show \"all\") sit beside the outer dots and\nabove the inner ones unless a point's labelPosition says otherwise.\nBars: sort \"desc\" orders by the first series; on a stacked bar add sortBy: \"total\" so the order is the\nvisible bar length. stacked: \"percent\" (bars) and stack: \"percent\" (areas) normalise every bar or x to\n100% so the segments read as shares; the axis becomes 0–100% and the hover shows share and value. colorBy \"sign\" (two colours around zero) or \"value\" (each bar shaded by its value along\npalette.sequential, or the diverging ramp when values span zero) — the ranked-and-shaded look\nwithout a categoryColors map by hand; it has no legend, so pair it with valueLabels: {show: \"all\"}.\n\nSETTINGS SHAPE\nA config is a document made of ELEMENTS. Every element has a few settings of its own and then\nsupports shared MODULES — font, box, position, stroke — each defined ONCE (they are named $defs in\nthe JSON Schema), the same wherever they appear. A module an element does not list is rejected there,\nnever ignored.\n  font      {family, size, weight, lineHeight, color, outline{width, color}}\n  box       {margin{top,right,bottom,left}, padding{…}, background, border{width, color, dash}, radius}\n  position  {space: canvas|plot|data, x, y, anchor, verticalAnchor, offset{x, y}}\n  stroke    {width, color, dash}\n  {document:    {aspect, box{padding, background, border, radius}},\n   title:       {text, font, align, box, position},\n   description: {...same...}, source: {text, url, ...same...}, notes: {...same...},\n   texts:       [{text, font, align, box (no margin), position (required)}],\n   legend:      {font, box, position},\n   badge:       {text, link, logoUrl, side, font, box, position},\n   chart:       {box{margin, background, border, radius}, its ELEMENTS, and the type's own options}\n   palette, colors, shape, encoding}\nA chart is elements too, and they differ by type — GET /api/asset-types/{type}/schema lists them:\n   xAxis / yAxis  {min, max, scale, ticks{count, format, show, labels{font}, marks{length, stroke}},\n                            gridlines{show, stroke}, line{show, stroke} (bars/waterfalls: zeroLine)}\n   seriesLabels (line, area) · valueLabels (bar, area, dumbbell, heatmap, waterfall) · pointLabels (scatter,\n   symbol-map) · regionLabels (choropleth, + color) · sliceLabels (pie, + values, leaderAnchor)   {show, font}\n   rowLabels (dumbbell) · labels + headers (slope) · header + cells (table)   {font}\n   colorBar (heatmap, choropleth) {show, height, width, radius, labelPosition, labels{font}}\n   outline (maps) {width, color} · centerLabel (pie) {text, font} · annotationStyle {font, align, box, leader, marker}\nThere is no chart.fonts and no chrome: a tick font is xAxis.ticks.labels.font, a grid is\nyAxis.gridlines, the baseline is xAxis.line (yAxis.zeroLine on bars).\ntitle/description/source/notes and every entry of texts[] are the SAME element type. The named ones\ndiffer only in semantics — title becomes the aria label, the OG title and the listing title. An\nelement with empty text is not drawn; there is no show/hide switch.\ntext: \n breaks a line; [words](#hex) colours a span; text wraps at its box — the document column in\nthe flow, the room to the canvas edge on the anchor's side when positioned. There is no wrap-width\nsetting. One title with a \n is one element — do NOT build a two-line title out of two texts[]\nentries, and use description for the subtitle: those stay one thing to edit, move and hide.\nalign (left|center|right) aligns the lines inside the block; a positioned element defaults to its anchor.\nRESTRAINT — one element per idea. A human will tune this chart in the editor afterwards, where every\nelement you add is one more panel to find. A multi-colour headline is ONE title with [spans](#hex)\nand \n, not stacked or overlaid texts[]; a running count along a line is data labels or one\nannotation at the end, not one annotation per year; a grid is yAxis.gridlines, not a reference line per\ntick; a table is the table type, not texts[] laid out by hand. Position fractions are stored to 4\ndecimals — the canvas is 640 units wide, so 0.03 is as exact as 0.034375; write positions with 2–3\ndecimals and nudge from there.\nDiscover it properly instead of guessing: GET /api/asset-types/{type}/schema serves the FULL JSON\nSchema, worked examples, AND \"defaults\" — the complete config in force when nothing is set (every\nfont, margin and color), so you read a baseline value instead of guessing it. GET /assets/{id}\nreturns the config in this same shape, so a read-edit-write loop stays in one vocabulary.\nThere is exactly ONE shape. Older shapes (frame / theme / options; padding, place, enabled, branding,\nchart.text, infoBoxes, dx/dy, textStyle, arrow) are rejected, and the error names the key's new home.\nErrors always report the paths you wrote (title.font.size, chart.labels).\nthan silently ignored.\n\nBRANDS\nA brand is default settings merged UNDER your config: any value the chart sets itself wins.\nThe mechanical difference between the two layers: brand values PROPAGATE — edit the brand and every\ndraft using it restyles, published charts pick it up on republish — while values written into the\nchart's own config are pinned to that one chart and never follow brand edits. Where you put a style\ndecides how it ages.\nPass brand: \"<slug>\" on create. Brand edits restyle drafts immediately; published assets pick them\nup on their next publish. Text styles accept ANY Google Fonts family by its exact name (fetched,\nmeasured, and cached server-side on first use); a family Google doesn't have fails loudly at brand\nsave with a FontUnavailableError — Inter, \"Source Serif 4\", and \"IBM Plex Mono\" are always available.\nA brand is DEFAULT SETTINGS in the SAME shape as a chart's config, minus the two things a brand\ncannot own: the words (no text/url on the elements) and the type-specific chart options. So it reads\nexactly like the config above — {document: {box}, title: {font, align, box, position},\ndescription/source/notes likewise, legend, badge, chart: {box + the style half of every element —\nxAxis.ticks.labels.font, yAxis.gridlines, seriesLabels.font, colorBar…}, palette, colors, shape}.\nResolution is ONE merge across three layers:  ENGINE_DEFAULTS <- brand <- the chart config.\nExample: more air above the title and a closer footer = {document: {box: {padding: {top: 28, bottom: 8}}},\nsource: {box: {margin: {top: 24}}}}.\nGET /api/brands/schema serves the generated JSON Schema — read it instead of guessing key names.\nGET /brands/{idOrSlug} returns the brand in the same shape. Unknown keys are rejected loudly at save\ntime, and retired shapes are rejected with the key's new home.\nTwo often-missed tokens: chart.xAxis.gridlines.show turns on vertical gridlines (line/area/scatter; off\nby default), and the diverging ramp flips by overriding it directly — e.g. blue=positive:\n{palette: {diverging: {negative: [reds...], positive: [blues...]}}} (swap the two ramps;\nheatmap, choropleth, and bar colorBy:\"sign\" all read them).\n\nERRORS\nunauthorized (check Authorization header) · validation_error (issues[] carries paths, enum options,\nand a schema link — fix and retry) · not_found (wrong id or other workspace) · version_conflict\n(see CONCURRENCY). Errors are never punitive; when stuck, POST /api/feedback so the owner can fix\nthe gap — do not silently give up.\n\nCREDITS (no subscriptions, no metering)\nHOSTING IS FREE: publish as many charts as you like — live-updating embeds, stable public URLs,\nfull-resolution PNGs (up to 2400px), with the chartlink badge. Republishing and data updates are\nNEVER metered on any tier (updating is the product). Credits buy PREMIUM, per chart, once: SVG\nexport, custom footer branding, no badge — that slot is premium forever. Packs: 1/$2.90, 10/$19, 50/$59.\nAn asset's hostedTier is \"trial\" (free hosting: badge, PNG to 1200px, no expiry — \"trial\" is the tier's\nname, not a countdown) or \"premium\".\nGET /api/billing-info shows credits. To buy: POST /api/checkout-link {\"pack\":\"1\"|\"10\"|\"50\"} returns\na payment URL to hand to your human — it opens straight into checkout (no login; the link can only\nadd credits to this workspace, so sharing it is safe). Upgrade any hosted chart in place with\nPOST /assets/{id}/premium (one credit, same URLs).\n\nCONVENTIONS\nList responses: {data, total, limit, offset}; list rows elide config/data (fetch one asset for full\nbodies). Timestamps are ISO-8601 UTC. Deletes are soft (POST /assets/{id}/restore). Data cap: 2 MB\nper asset — aggregate, don't paginate charts."}]},"jsonrpc":"2.0","id":6},"http_status":200,"headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"},"resource_uri":"chartlink://llms.txt"}},"determinism_probe":{"status":"ok","latency_ms":427.98,"details":{"attempts":2,"successful":2,"matches":2,"stable_ratio":1.0,"baseline_signature":"958746dde965b4467f378b8cb0aed4ecbb44138f31c1a22aec24f000ef3d41e5","errors":[]}},"instruction_tool_reference_probe":{"status":"ok","latency_ms":null,"details":{"referenced_tools":["signup"],"missing_tools":[],"observed_tool_count":10}},"session_resume_probe":{"status":"warning","latency_ms":null,"details":{"reason":"no_session_id","transport":"streamable-http","resume_expected":true,"protocol_version":"2025-03-26"}},"step_up_auth_probe":{"status":"missing","latency_ms":null,"details":{"oauth_present":false,"auth_required_checks":[],"supported_scopes":[],"scope_specificity_ratio":0.0,"broad_scopes":[],"challenge_headers":[],"step_up_signals":[],"minimal_scope_documented":false}},"transport_compliance_probe":{"status":"warning","latency_ms":48.26,"details":{"transport":"streamable-http","session_id_present":false,"protocol_header_present":false,"last_event_id_visible":false,"requested_protocol_version":"2025-03-26","bad_protocol_status_code":400,"bad_protocol_payload":{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Unsupported protocol version: 1999-99-99 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"},"id":null},"bad_protocol_headers":{"content-type":"application/json","strict-transport-security":"max-age=63072000"},"bad_protocol_error":null,"delete_status_code":null,"delete_error":null,"expired_session_status_code":null,"expired_session_error":null,"issues":["missing_session_id","missing_protocol_header"]}},"utility_coverage_probe":{"status":"missing","latency_ms":99.78,"details":{"completions":{"advertised":false,"sample_target":{"type":"prompt","name":"make-a-chart","argument_name":"brief"},"live_probe":"not_executed"},"pagination":{"supported":false,"next_cursor_methods":[],"metadata_signal":false},"tasks":{"advertised":false,"probe_status":"missing","http_status":200},"initialize_capability_keys":["prompts","resources","tools"]}},"advanced_capabilities_probe":{"status":"warning","latency_ms":null,"details":{"capabilities":{"prompts":true,"resources":true,"completions":false,"roots":false,"sampling":false,"elicitation":false,"structured_outputs":false,"resource_links":true},"enabled_count":3,"enabled":["prompts","resource_links","resources"],"initialize_capability_keys":["prompts","resources","tools"]}},"tool_snapshot_probe":{"status":"ok","latency_ms":null,"details":{"current_tool_count":10,"previous_tool_count":10,"similarity":1.0,"added":[],"removed":[],"changed_outputs":[]}},"connector_replay_probe":{"status":"ok","latency_ms":null,"details":{"backward_compatible":true,"would_break_after_refresh":false,"added_tools":[],"removed_tools":[],"required_arg_breaks":[],"output_breaks":[],"additive_output_changes":[]}},"request_association_probe":{"status":"missing","latency_ms":null,"details":{"reason":"no_request_association_capabilities_advertised"}},"interactive_flow_probe":{"status":"ok","latency_ms":null,"details":{"risk_hits":[],"safe_hits":["login"],"oauth_supported":false,"prompt_available":false}},"action_safety_probe":{"status":"error","latency_ms":null,"details":{"summary":{"tool_count":10,"high_risk_tools":1,"destructive_tools":3,"exec_tools":1,"egress_tools":0,"secret_tools":0,"bulk_access_tools":0,"declared_non_read_only_tools":7,"annotation_conflict_tools":0,"risk_distribution":{"low":3,"medium":6,"high":1,"critical":0},"capability_distribution":{"write":7,"read":3,"delete":3,"exec":1},"has_mutating_capability":true,"has_non_read_capability":true},"auth_present":false,"safeguard_count":0,"confirmation_signals":["create_from_template"],"reason":null}},"official_registry_probe":{"status":"missing","latency_ms":null,"details":{"registry_source":"awesome_mcp_servers","direct_match":false,"official_peer_count":0}},"provenance_divergence_probe":{"status":"not_assessed","latency_ms":null,"details":{"direct_official_match":false,"registry_title":null,"server_card_title":null,"registry_version":null,"server_card_version":null,"registry_homepage":null,"server_card_homepage":null,"registry_repository":null,"server_card_repository":null,"drift_fields":[],"metadata_document_count":1,"compared_fields":["title","version","homepage","repository"],"readable_sources":[],"comparable_field_count":0}},"schema_divergence_probe":{"status":"missing","latency_ms":null,"details":{"reason":"no_server_card_tools","compared_tool_count":0,"compared_dimensions":["server_name","server_version","declared_vs_observed_auth","tool_membership","parameter_names","required_parameters","parameter_types","output_schema_presence"],"server_name_mismatch":false,"card_server_name":null,"live_server_name":"chartlink","server_version_mismatch":false,"card_server_version":null,"live_server_version":"0.1.0","auth_scheme_mismatch":false}},"connector_publishability_probe":{"status":"warning","latency_ms":null,"details":{"transport":"streamable-http","tool_count":10,"high_risk_tools":1,"blockers":["protocol_version","session_resume","step_up_auth","transport_compliance","request_association","action_safety","server_card"],"criteria":{"remote_transport":true,"initialize":true,"tools_list":true,"protocol_version":false,"session_resume":false,"step_up_auth":false,"transport_compliance":false,"connector_replay":true,"request_association":false,"action_safety":false,"server_card":false,"tool_surface":true,"auth_flow":true}}}}},"active_alerts":[],"maintainer_analytics":{},"public_server_reputation":{},"maintainer_response_quality":{},"maintainer_annotations":[],"maintainer_rebuttals":[],"security_posture_summary":{},"tool_security_inventory":[],"transport_compliance":{},"utility_coverage":{},"write_action_governance":{},"provenance_divergence":{},"alias_consolidation":{},"alert_routing":{},"authenticated_validation":{},"hosted_runtime":{},"action_controls_diff":null,"benchmark_tasks":[],"latest_capability_counts":{},"point_loss_breakdown":[],"verdict_traces":{},"current_snapshot":{"schema_version":"verify.trust_snapshot.v1","snapshot_id":"trustsnap_d483a434f7295804","generated_at":"2026-09-29T00:41:39.070673+00:00","trust_evaluated_at":"2026-09-29T00:41:39.070673+00:00","evidence_revision":"0ffb9d510a3b31901b7b3df1","source":"current_snapshot","server":"awesome-oscarleoo/chartlink-agents","last_validated_at":"2026-09-28T16:15:12.257869+00:00","validation_age_hours":8.44,"freshness":{"schema_version":"verify.freshness_profile.v1","last_validated_at":"2026-09-28T16:15:12.257869+00:00","age_hours":8.44,"bucket":"verified_last_24h","label":"Verified in last 24h","badges":["verified_last_24h"],"freshness_sla_hours":720.0,"freshness_sla_status":"met","stale_score_suppressed":false,"display_score":61.98,"raw_score":61.98,"confidence_score":95.0,"confidence_weighted_score":58.9,"tier_status":[{"tier":"community","label":"Community","freshness_sla_hours":720,"met":true,"priority_revalidation":false},{"tier":"pro","label":"Pro","freshness_sla_hours":168,"met":true,"priority_revalidation":true},{"tier":"enterprise","label":"Enterprise","freshness_sla_hours":24,"met":true,"priority_revalidation":true}]},"current_status":"healthy","current_score":61.98,"display_score":61.98,"stale_score_suppressed":false,"production_trust_decision":{"schema_version":"verify.executive_verdict.v1","decision":"Allow with approval","why":"write/admin actions need confirmation safeguards","next_action":"export policy, require approval for writes, add authenticated validation","reason_count":1},"production_readiness_class":{"code":"safe_for_evaluation","label":"Safe for evaluation","reason":"The server is suitable for evaluation, but remaining gaps should be resolved before broad production use."},"evidence_confidence":{"score":95.0,"label":"high","validation_age_hours":8.44,"live_check_count":16,"basis":{"evidence_bearing_validations":16,"affirmative_live_check_count":16,"validation_age_hours":8.44,"freshness_threshold_hours":24}},"active_alerts":[],"active_alert_summary":{"critical":0,"high":0,"medium":0,"low":0,"high_or_critical":0,"total":0},"materialization":{"state":"partial","trust_core_complete":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"materialized_at":"2026-09-29T00:41:39.070673+00:00"}},"trust_snapshot":{"schema_version":"verify.trust_snapshot.v1","snapshot_id":"trustsnap_d483a434f7295804","generated_at":"2026-09-29T00:41:39.070673+00:00","trust_evaluated_at":"2026-09-29T00:41:39.070673+00:00","evidence_revision":"0ffb9d510a3b31901b7b3df1","source":"current_snapshot","server":"awesome-oscarleoo/chartlink-agents","last_validated_at":"2026-09-28T16:15:12.257869+00:00","validation_age_hours":8.44,"freshness":{"schema_version":"verify.freshness_profile.v1","last_validated_at":"2026-09-28T16:15:12.257869+00:00","age_hours":8.44,"bucket":"verified_last_24h","label":"Verified in last 24h","badges":["verified_last_24h"],"freshness_sla_hours":720.0,"freshness_sla_status":"met","stale_score_suppressed":false,"display_score":61.98,"raw_score":61.98,"confidence_score":95.0,"confidence_weighted_score":58.9,"tier_status":[{"tier":"community","label":"Community","freshness_sla_hours":720,"met":true,"priority_revalidation":false},{"tier":"pro","label":"Pro","freshness_sla_hours":168,"met":true,"priority_revalidation":true},{"tier":"enterprise","label":"Enterprise","freshness_sla_hours":24,"met":true,"priority_revalidation":true}]},"current_status":"healthy","current_score":61.98,"display_score":61.98,"stale_score_suppressed":false,"production_trust_decision":{"schema_version":"verify.executive_verdict.v1","decision":"Allow with approval","why":"write/admin actions need confirmation safeguards","next_action":"export policy, require approval for writes, add authenticated validation","reason_count":1},"production_readiness_class":{"code":"safe_for_evaluation","label":"Safe for evaluation","reason":"The server is suitable for evaluation, but remaining gaps should be resolved before broad production use."},"evidence_confidence":{"score":95.0,"label":"high","validation_age_hours":8.44,"live_check_count":16,"basis":{"evidence_bearing_validations":16,"affirmative_live_check_count":16,"validation_age_hours":8.44,"freshness_threshold_hours":24}},"active_alerts":[],"active_alert_summary":{"critical":0,"high":0,"medium":0,"low":0,"high_or_critical":0,"total":0},"materialization":{"state":"partial","trust_core_complete":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"materialized_at":"2026-09-29T00:41:39.070673+00:00"}},"agent_commerce":{},"latest_claim":null,"maintainer_profile_slug":null,"watch_summary":{},"partial":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"trust_evaluated_at":"2026-09-29T00:41:39.070673+00:00","evidence_revision":"0ffb9d510a3b31901b7b3df1","active_alert_summary":{"critical":0,"high":0,"medium":0,"low":0,"high_or_critical":0,"total":0},"materialization":{"state":"partial","trust_core_complete":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"materialized_at":"2026-09-29T00:41:39.070673+00:00"},"owner_opted_out":false},"latest_validation":null,"current_snapshot":{"schema_version":"verify.trust_snapshot.v1","snapshot_id":"trustsnap_d483a434f7295804","generated_at":"2026-09-29T00:41:39.070673+00:00","trust_evaluated_at":"2026-09-29T00:41:39.070673+00:00","evidence_revision":"0ffb9d510a3b31901b7b3df1","source":"current_snapshot","server":"awesome-oscarleoo/chartlink-agents","last_validated_at":"2026-09-28T16:15:12.257869+00:00","validation_age_hours":8.44,"freshness":{"schema_version":"verify.freshness_profile.v1","last_validated_at":"2026-09-28T16:15:12.257869+00:00","age_hours":8.44,"bucket":"verified_last_24h","label":"Verified in last 24h","badges":["verified_last_24h"],"freshness_sla_hours":720.0,"freshness_sla_status":"met","stale_score_suppressed":false,"display_score":61.98,"raw_score":61.98,"confidence_score":95.0,"confidence_weighted_score":58.9,"tier_status":[{"tier":"community","label":"Community","freshness_sla_hours":720,"met":true,"priority_revalidation":false},{"tier":"pro","label":"Pro","freshness_sla_hours":168,"met":true,"priority_revalidation":true},{"tier":"enterprise","label":"Enterprise","freshness_sla_hours":24,"met":true,"priority_revalidation":true}]},"current_status":"healthy","current_score":61.98,"display_score":61.98,"stale_score_suppressed":false,"production_trust_decision":{"schema_version":"verify.executive_verdict.v1","decision":"Allow with approval","why":"write/admin actions need confirmation safeguards","next_action":"export policy, require approval for writes, add authenticated validation","reason_count":1},"production_readiness_class":{"code":"safe_for_evaluation","label":"Safe for evaluation","reason":"The server is suitable for evaluation, but remaining gaps should be resolved before broad production use."},"evidence_confidence":{"score":95.0,"label":"high","validation_age_hours":8.44,"live_check_count":16,"basis":{"evidence_bearing_validations":16,"affirmative_live_check_count":16,"validation_age_hours":8.44,"freshness_threshold_hours":24}},"active_alerts":[],"active_alert_summary":{"critical":0,"high":0,"medium":0,"low":0,"high_or_critical":0,"total":0},"materialization":{"state":"partial","trust_core_complete":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"materialized_at":"2026-09-29T00:41:39.070673+00:00"}},"trust_snapshot":{"schema_version":"verify.trust_snapshot.v1","snapshot_id":"trustsnap_d483a434f7295804","generated_at":"2026-09-29T00:41:39.070673+00:00","trust_evaluated_at":"2026-09-29T00:41:39.070673+00:00","evidence_revision":"0ffb9d510a3b31901b7b3df1","source":"current_snapshot","server":"awesome-oscarleoo/chartlink-agents","last_validated_at":"2026-09-28T16:15:12.257869+00:00","validation_age_hours":8.44,"freshness":{"schema_version":"verify.freshness_profile.v1","last_validated_at":"2026-09-28T16:15:12.257869+00:00","age_hours":8.44,"bucket":"verified_last_24h","label":"Verified in last 24h","badges":["verified_last_24h"],"freshness_sla_hours":720.0,"freshness_sla_status":"met","stale_score_suppressed":false,"display_score":61.98,"raw_score":61.98,"confidence_score":95.0,"confidence_weighted_score":58.9,"tier_status":[{"tier":"community","label":"Community","freshness_sla_hours":720,"met":true,"priority_revalidation":false},{"tier":"pro","label":"Pro","freshness_sla_hours":168,"met":true,"priority_revalidation":true},{"tier":"enterprise","label":"Enterprise","freshness_sla_hours":24,"met":true,"priority_revalidation":true}]},"current_status":"healthy","current_score":61.98,"display_score":61.98,"stale_score_suppressed":false,"production_trust_decision":{"schema_version":"verify.executive_verdict.v1","decision":"Allow with approval","why":"write/admin actions need confirmation safeguards","next_action":"export policy, require approval for writes, add authenticated validation","reason_count":1},"production_readiness_class":{"code":"safe_for_evaluation","label":"Safe for evaluation","reason":"The server is suitable for evaluation, but remaining gaps should be resolved before broad production use."},"evidence_confidence":{"score":95.0,"label":"high","validation_age_hours":8.44,"live_check_count":16,"basis":{"evidence_bearing_validations":16,"affirmative_live_check_count":16,"validation_age_hours":8.44,"freshness_threshold_hours":24}},"active_alerts":[],"active_alert_summary":{"critical":0,"high":0,"medium":0,"low":0,"high_or_critical":0,"total":0},"materialization":{"state":"partial","trust_core_complete":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"materialized_at":"2026-09-29T00:41:39.070673+00:00"}},"partial":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"snapshot_invariant":{"schema_version":"verify.snapshot_invariant.v1","server":"awesome-oscarleoo/chartlink-agents","ok":true,"surface_snapshot_ids":{"page":"trustsnap_d483a434f7295804","badge":null,"report":"trustsnap_d483a434f7295804","policy":null},"checked_surfaces":["page","report"],"unchecked_surfaces":["badge","policy"],"checked_count_surfaces":[],"count_mismatches":{},"checked_at":"2026-09-29T00:41:39.062494+00:00"},"history":{},"production_readiness":{"code":"safe_for_evaluation","label":"Safe for evaluation","reason":"The server is suitable for evaluation, but remaining gaps should be resolved before broad production use.","badge":"score-medium","critical_alerts":0,"high_or_critical_alerts":0,"degraded_by_active_alerts":false},"agent_commerce_readiness":{},"evidence_confidence":{"score":95.0,"label":"high","reason":"Based on 16 recent validations, 16 captured checks, and validation age of 8.4 hours.","live_check_count":16,"validation_age_hours":8.44,"basis":{"evidence_bearing_validations":16,"affirmative_live_check_count":16,"validation_age_hours":8.44,"freshness_threshold_hours":24}},"recommended_for":[],"active_alerts":[],"remediations":[],"cache_note":"Fast fallback response; full report evidence is deferred to protect web capacity. Fields listed in fields_unavailable (including tool_security_inventory, and write_action_governance) are not yet computed on this response -- an empty value means unchecked, not confirmed clean. Treat partial responses as indeterminate.","publisher_claim":{"verified":false,"status":"unclaimed","claim_url":"https://verify.sentinelsignal.io/claim?server=awesome-oscarleoo%2Fchartlink-agents&source=report_json","reason_code":"claim_to_publish_metadata","reason":"Claim this profile to verify publisher identity, add evidence, and manage trust metadata.","score_neutral":true},"observed_attention":{"schema":"verify.observed_attention.v1","window_days":30,"level":"none","label":"No observed attention","summary":"No recent machine-readable trust or discovery activity observed for this server.","segments":{"useful_ai_user":{"level":"none","observed":false,"description":"AI-assisted user sessions such as ChatGPT/User or Claude/User."},"machine_trust_evaluator":{"level":"none","observed":false,"description":"Synthetic sessions inspecting multiple trust surfaces such as report, policy, ledger, badge, trust-summary, or compare."},"possible_agent_or_script":{"level":"none","observed":false,"description":"Structured direct sessions with rapid profile, compare, report, policy, badge, or trust-surface fan-out."},"isolated_machine_surface":{"level":"none","observed":false,"description":"Aged-out direct synthetic singleton sessions that touched a machine-readable trust surface without becoming a broader evaluator."},"ai_crawler":{"level":"none","observed":false,"description":"Known AI crawler activity such as ClaudeBot, GPTBot, or similar crawlers."},"search_crawler":{"level":"none","observed":false,"description":"Search and SEO crawler activity."},"browser_like_automation":{"level":"none","observed":false,"description":"Browser-like synthetic sessions with rapid structured endpoint activity."},"confirmed_human":{"level":"none","observed":false,"description":"Confirmed browser-session human activity."}},"surfaces_observed":{"server_profile":false,"compare":false,"compare_json":false,"compare_api":false,"report_json":false,"policy":false,"ledger":false,"badge_metadata":false,"badge_svg":false,"trust_summary":false,"mcp_tool":false},"claim_prompt":{"recommended":false,"reason":"No claim prompt is recommended from observed attention in the current window."},"notes":["Observed attention is based on segmented first-party telemetry.","Crawler and evaluator activity is not treated as confirmed human demand.","Public levels are bucketed to avoid exposing raw traffic counts."]},"owner_activation":{"claim_recommended":false,"reason":"no_observed_attention"},"related_machine_surfaces":{"compare_index":"/compare.json","compare_api":"/v1/compare?server=awesome-oscarleoo%2Fchartlink-agents","trust_summary":"/v1/servers/awesome-oscarleoo/chartlink-agents/trust-summary","ledger":"/v1/servers/awesome-oscarleoo/chartlink-agents/ledger","policy":"/v1/servers/awesome-oscarleoo/chartlink-agents/policy","report":"/v1/servers/awesome-oscarleoo/chartlink-agents/report"},"intelligence_api":{"available":true,"signup_url":"https://verify.sentinelsignal.io/verify-intelligence-api","use_case":"Programmatic MCP server trust, comparison, policy, and evidence enrichment."},"trust_evaluated_at":"2026-09-29T00:41:39.070673+00:00","evidence_revision":"0ffb9d510a3b31901b7b3df1","active_alert_summary":{"critical":0,"high":0,"medium":0,"low":0,"high_or_critical":0,"total":0},"materialization":{"state":"partial","trust_core_complete":true,"fields_unavailable":["tool_security_inventory","security_posture_summary","write_action_governance","capability_taxonomy","remediations"],"materialized_at":"2026-09-29T00:41:39.070673+00:00"}}