Adoption · Embed recipes · v0.1.0
Publish on any host. In ten minutes.
An agentic-first profile is one small JSON file. The hardest
part is wrangling whatever CMS you happen to use into serving
it at the canonical URL with the right
Content-Type. This page is the copy-paste recipe
for every common host - file-based, CMS-embedded, and the
inline-XML fallback for the few platforms that strip
everything. Pick your host, paste, deploy, submit. Done.
Pick the right mode
Three modes, in order of preference. Use the highest one your host supports. The directory tries them in this order on submission and uses the first one it finds.
| Mode | Where the profile lives | Use it if | Trade-off |
|---|---|---|---|
| 1. File (canonical) | https://yourdomain/.well-known/agentic-profile.json |
Your host lets you upload an arbitrary file to a path beginning with a dot, OR you control a build pipeline that does. | None. This is the spec. Maximum compatibility. |
| 2. Embed (data island) | <script type="application/agentic-profile+json"> on your home page |
Your host lets you inject HTML into <head> or a code-block, but won't let you upload a file at /.well-known/. |
Adds a few KB to your home page weight. Pair with a <link rel="agentic-profile"> for explicit discovery. |
| 3. Inline XML (last resort) | Hidden <div hidden id="agentic-profile" data-format="xml"> block |
Your host strips <script> tags or re-encodes JSON, AND you can still inject any raw HTML. |
Directory parses it but flags a soft warning. JSON is the canonical wire format; this exists so that no host is left out. |
Worked example - the same profile rendered three ways:
// Mode 1: file at /.well-known/agentic-profile.json
{
"schema_version": "0.1.0",
"updated_at": "2026-04-19T12:00:00Z",
"profile_kind": "company",
"tier": "public",
"company": {
"name": "Acme Robotics",
"website": "https://acme-robotics.example",
"jurisdiction": "GB"
}
}
<!-- Mode 2: embed inside <head> or just before </body> -->
<script type="application/agentic-profile+json">
{
"schema_version": "0.1.0",
"updated_at": "2026-04-19T12:00:00Z",
"profile_kind": "company",
"tier": "public",
"company": {
"name": "Acme Robotics",
"website": "https://acme-robotics.example",
"jurisdiction": "GB"
}
}
</script>
<link rel="agentic-profile"
type="application/json"
href="/.well-known/agentic-profile.json">
<!-- Mode 3: inline XML, anywhere on the page -->
<div hidden id="agentic-profile" data-format="xml">
<agentic-profile version="0.1.0" kind="company" tier="public">
<company>
<name>Acme Robotics</name>
<website>https://acme-robotics.example</website>
<jurisdiction>GB</jurisdiction>
</company>
<updated_at>2026-04-19T12:00:00Z</updated_at>
</agentic-profile>
</div>
File-host recipes
Mode 1 - the canonical path. Recipes are in rough order of
adoption. Every recipe ends with the same verification step:
curl -I https://yourdomain/.well-known/agentic-profile.json
should return 200 with
Content-Type: application/json.
Raw HTML / VPS
Drop the file in your document root under
/.well-known/agentic-profile.json. That's the
whole recipe. The only thing worth checking is the response
Content-Type: most servers infer
application/json from the .json
extension; if yours doesn't, see Apache / Nginx / Caddy
below.
Apache
# /var/www/html/.well-known/agentic-profile.json (the file)
# /var/www/html/.htaccess (only needed if your host blocks dotfiles by default)
<Files "agentic-profile.json">
Header set Content-Type "application/json"
Header set Cache-Control "public, max-age=300"
</Files>
# Some shared hosts block /.well-known/ entirely. If yours does, add:
<Directory "/var/www/html/.well-known">
Require all granted
</Directory>
Nginx
server {
# ...your existing server block...
location = /.well-known/agentic-profile.json {
default_type application/json;
add_header Cache-Control "public, max-age=300";
add_header Access-Control-Allow-Origin *; # so browser-side agents can read it
}
}
Caddy
yourdomain.example {
encode gzip
root * /var/www/html
@profile path /.well-known/agentic-profile.json
header @profile Content-Type application/json
header @profile Cache-Control "public, max-age=300"
file_server
}
Vercel
Two ways. Pick whichever fits your project.
(a) Static file in public/. Drop
the JSON at public/.well-known/agentic-profile.json.
Vercel serves public/ at the root, so this
becomes /.well-known/agentic-profile.json
automatically. Add a vercel.json only if you need
to force the header (Vercel infers JSON correctly by default):
{
"headers": [
{
"source": "/.well-known/agentic-profile.json",
"headers": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Cache-Control", "value": "public, max-age=300" }
]
}
]
}
(b) Edge function. If you want to assemble
the profile at request time (e.g. inject a build hash into
updated_at), drop a app/.well-known/agentic-profile.json/route.ts
file with a single GET handler that returns the JSON.
Netlify
Same as Vercel - drop the file at
public/.well-known/agentic-profile.json (or
whatever your publish dir is in
netlify.toml). To pin the headers, use
_headers:
# public/_headers
/.well-known/agentic-profile.json
Content-Type: application/json
Cache-Control: public, max-age=300
Access-Control-Allow-Origin: *
Cloudflare Pages
Drop the file at public/.well-known/agentic-profile.json
(or whatever you set as build.output). Headers
via a public/_headers file (same syntax as Netlify):
# public/_headers
/.well-known/agentic-profile.json
Content-Type: application/json
Cache-Control: public, max-age=300
GitHub Pages
Two gotchas. First: Jekyll (the default GitHub Pages
processor) excludes dotfiles by default, so
.well-known/ won't ship unless you tell it to.
Second: GitHub Pages serves .json as
application/json automatically, so you don't need
to set headers - but you can't customise them either.
# _config.yml
include:
- .well-known
# Then commit:
# .well-known/agentic-profile.json
# at the root of your repo (or under docs/ if you serve from there).
Pages built with the no-Jekyll path
(.nojekyll file present): just commit
.well-known/agentic-profile.json; Pages will serve
it as-is.
AWS S3 + CloudFront
- Upload the file to your bucket at
.well-known/agentic-profile.json. - Set the object's
Content-Typemetadata toapplication/json(S3 infers this from the extension on most upload paths, but the AWS CLI--content-typeflag is the safest):aws s3 cp agentic-profile.json \ s3://yourbucket/.well-known/agentic-profile.json \ --content-type application/json \ --cache-control "public, max-age=300" - If CloudFront is in front of S3, you don't need a
behaviour-level override; the
Content-Typefrom the origin passes through. If you have a custom error page on 404, double-check it doesn't catch/.well-known/agentic-profile.jsonbefore S3 sees it.
Azure Static Web Apps
Drop the file at public/.well-known/agentic-profile.json
(or wherever your app_artifact_location points).
Azure Static Web Apps serves .well-known/ paths
out of the box; configure headers in
staticwebapp.config.json if you want to pin them:
{
"routes": [
{
"route": "/.well-known/agentic-profile.json",
"headers": {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300"
}
}
]
}
Fly.io / Railway / Render
These run your container; serve the file however your app framework normally serves a static file. For an existing Express / FastAPI / Django app, mount the file as a static route:
// Express
app.get("/.well-known/agentic-profile.json", (_req, res) =>
res.type("application/json").sendFile("/app/agentic-profile.json"));
// FastAPI
@app.get("/.well-known/agentic-profile.json", response_class=JSONResponse)
def agentic_profile():
return JSONResponse(profile_dict, media_type="application/json")
Static-site generators
| Generator | File path | Notes |
|---|---|---|
| Astro | public/.well-known/agentic-profile.json | Served as-is. No config needed. |
| Next.js | public/.well-known/agentic-profile.json | Served as-is. For dynamic profile, use app/.well-known/agentic-profile.json/route.ts. |
| SvelteKit | static/.well-known/agentic-profile.json | Served as-is. Adapter-static + adapter-vercel both work. |
| Nuxt | public/.well-known/agentic-profile.json | Served as-is. (Nuxt 3.) |
| Hugo | static/.well-known/agentic-profile.json | Hugo copies static/ verbatim; dotfiles included by default. |
| Jekyll | Repo root .well-known/agentic-profile.json + add include: [.well-known] to _config.yml | Jekyll excludes dotfiles by default - the include line is required. |
| Eleventy | src/.well-known/agentic-profile.json + passthrough copy | Add eleventyConfig.addPassthroughCopy(".well-known") to .eleventy.js. |
| Gatsby | static/.well-known/agentic-profile.json | Served as-is. |
| Docusaurus | static/.well-known/agentic-profile.json | Served as-is. |
| Astro (multipage build) | public/.well-known/agentic-profile.json | Same as single-page; Astro's public/ is verbatim. |
Cloudflare Worker (in front of any host)
Universal escape hatch. If your CMS won't let you upload a
file under /.well-known/, put your domain behind
Cloudflare (DNS-only or full proxy is fine), then deploy this
one-file Worker:
// worker.js - serve /.well-known/agentic-profile.json,
// fall through to your real host for everything else.
const PROFILE = JSON.stringify({
schema_version: "0.1.0",
updated_at: "2026-04-19T12:00:00Z",
profile_kind: "company",
tier: "public",
company: {
name: "Acme Robotics",
website: "https://acme-robotics.example",
jurisdiction: "GB"
}
});
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/.well-known/agentic-profile.json") {
return new Response(PROFILE, {
headers: {
"content-type": "application/json",
"cache-control": "public, max-age=300",
"access-control-allow-origin": "*",
},
});
}
return fetch(request);
},
};
Bind the Worker to yourdomain.example/.well-known/agentic-profile.json
(or the broader * route - it falls through). This
gives you the canonical URL and a proper
Content-Type, with no surgery on your CMS.
CMS-embed recipes
Mode 2 - the data-island. Use these when your host won't let
you serve a file under /.well-known/ but does
let you inject HTML into <head> or a code
block. Pair every recipe below with a discovery
<link> tag so agents and the directory can
find the data without parsing the whole page.
Snippet to paste (used by every recipe in this section - replace the JSON body with your actual profile):
<script type="application/agentic-profile+json">
{
"schema_version": "0.1.0",
"updated_at": "2026-04-19T12:00:00Z",
"profile_kind": "company",
"tier": "public",
"company": {
"name": "Acme Robotics",
"website": "https://acme-robotics.example",
"jurisdiction": "GB"
}
}
</script>
<link rel="agentic-profile"
type="application/json"
href="/.well-known/agentic-profile.json">
WordPress
Three install paths. The Code Snippets one is the safest if you're not comfortable editing theme files.
(a) Code Snippets plugin (recommended for non-devs). Install the free Code Snippets plugin. Add a new snippet, set "Run snippet everywhere" → "Front-end only" → "PHP", paste:
<?php
add_action('wp_head', function () {
?>
<script type="application/agentic-profile+json">
{ /* your profile JSON here */ }
</script>
<link rel="agentic-profile"
type="application/json"
href="/.well-known/agentic-profile.json">
<?php
});
(b) Child theme functions.php.
Same code, in your child theme's functions.php.
(c) File mode (better, if you can).
WordPress hosts on most managed WP platforms (WP Engine,
Kinsta, Pressable) do let you upload a file under
/.well-known/ via SFTP. If yours does, do that and
use the file mode instead - one fewer thing to break on theme
updates.
Squarespace
Squarespace is the trickiest popular host (no dotfile uploads,
no per-page Content-Type control). Three options
in increasing order of robustness:
(1) Cloudflare Worker in front - see the Worker recipe above. Recommended.
(2) Code Injection: Settings → Advanced →
Code Injection → HEADER, paste the
<script type="application/agentic-profile+json">
block plus the discovery <link>. Save.
Works on every Squarespace template that doesn't strip script
tags (almost all do allow them in HEADER injection).
(3) Subdomain on a static host: e.g.
profile.yourdomain.example as a CNAME to
Cloudflare Pages serving the JSON. Set
contact.private_mcp and
company.website in your profile to your real
Squarespace URL.
Wix
Settings → Custom Code → Add Custom Code. Paste the
<script type="application/agentic-profile+json">
+ discovery <link>. Set Place code
in = Head, Add code to = All
pages, Load code on = Each new page.
Wix lets you set your domain in front of a Cloudflare Worker,
so the Worker recipe also applies. If you're on Wix Studio or
Velo, you can also serve the JSON directly via an
http-functions.js backend file - that gives you a
proper Content-Type too.
Webflow
Project Settings → Custom Code → Head Code. Paste the script + link block. Publish. Done.
Note: Webflow's free plan has a 10 KB limit on the head-code field; a fully-fleshed profile easily fits, but if yours doesn't, use the Worker recipe instead.
Ghost
Settings → Code Injection → Site Header. Paste the script + link block. Save.
Ghost also lets you serve files via a self-hosted install - if you control the box, prefer the file mode.
Shopify
Online Store → Themes → Edit Code → theme.liquid.
Just before </head>, paste the script +
link block. Save.
Shopify Plus stores can use a Script Tag via the Storefront API instead, which survives theme swaps. Either works.
Notion (via Super.so / Potion / Fruition)
Vanilla Notion pages can't carry custom HTML, but the common Notion-as-website wrappers can:
- Super.so: Settings → Code → Head. Paste the script + link block.
- Potion.so: Settings → Custom Code → Head. Same.
- Fruition: edit the
worker.js, append the script tag to the injected HTML head. - Vanilla Notion (no wrapper): not supported - paste the JSON into a code block as documentation, but the directory won't pick it up.
Carrd
Pro plan only (the free tier doesn't allow custom code). Site Settings → Embed → Head. Paste the script + link block. Save.
Substack
Substack does not let you inject arbitrary HTML into the page head on the free tier. On Custom Domain, you can put a Cloudflare Worker in front (see the Worker recipe) and serve the well-known path there. That's the only Substack-compatible option today.
Constrained-host recipes
Mode 3 - the inline-XML fallback. Use this only when your
host strips <script> tags AND won't let
you put a Cloudflare Worker in front. The XML form mirrors
the JSON one-for-one; the directory accepts it but flags a
soft warning recommending an upgrade to mode 1 or 2.
Google Sites
Google Sites is the worst common host for this - no dotfile upload, no head-code injection, script tags filtered. You have two paths:
(a) Cloudflare Worker in front - if you can put your domain on Cloudflare. This is the strongly recommended path; the Worker handles everything and Google Sites stays untouched.
(b) Embed widget with raw HTML. Insert →
Embed → Embed code, paste an HTML snippet containing the
inline-XML form below. The widget serves it inside an
<iframe>, so the directory has to do extra
work to find it; coverage isn't guaranteed. Treat as
best-effort.
<div hidden id="agentic-profile" data-format="xml">
<agentic-profile version="0.1.0" kind="company" tier="public">
<company>
<name>Acme Robotics</name>
<website>https://acme-robotics.example</website>
<jurisdiction>GB</jurisdiction>
</company>
<updated_at>2026-04-19T12:00:00Z</updated_at>
</agentic-profile>
</div>
Linktree / Beacons / Bio.link
None of these let you inject arbitrary HTML on the page head. Practical option: register a real domain (most of these services let you set a custom domain), point that at a Cloudflare Worker, and let the Worker serve the well-known path. Linktree-as-real-website is not viable for agentic-first; treat the bio link as a CTA toward your real profile URL on a different host.
Medium
Medium doesn't allow custom HTML in posts and doesn't let you
override the publication's head. If you have a custom domain
on Medium, use the Cloudflare Worker recipe. Otherwise, host
the profile on a separate static host
(profile.yourname.example) and link to it from
your Medium about page.
Genuinely no-code-allowed hosts
If your host allows neither custom files at
/.well-known/, nor head-code injection, nor a
Cloudflare Worker, nor a custom domain - you cannot publish an
agentic-first profile from that host. The cheapest fix is to
register one domain, host the profile on any static service
from the file-host list, and use
that as your canonical online identity. The directory's
domain-pointer convention then ties it back to whichever
no-code host you actually use day-to-day.
Verify your profile is reachable
Before submitting to the directory, run these three checks from your laptop:
# 1. Status + content-type
curl -I https://yourdomain.example/.well-known/agentic-profile.json
# Expect: HTTP/2 200, content-type: application/json
# 2. Body parses as JSON
curl -sS https://yourdomain.example/.well-known/agentic-profile.json | jq .
# 3. Body validates against the canonical schema (requires the pitch CLI)
pip install pitch-cli # one-time
pitch-mcp validate https://yourdomain.example
If you used embed mode, verify the
<script type="application/agentic-profile+json">
is present in your home page's HTML:
curl -sSL https://yourdomain.example/ \
| grep -A 30 'application/agentic-profile+json'
Submit to the directory
curl -sS -X POST https://directory.agentic-first.co/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"submit_website",
"arguments":{"domain":"yourdomain.example"}}}'
Or call submit_website from any MCP-aware client
(Claude Desktop, Cursor, Codex CLI) pointed at
https://directory.agentic-first.co/mcp. The
directory will discover the profile via mode 1 → 2 → 3 in
that order, validate it, and index it. If it can't find
anything, you'll get a structured error telling you which
mode failed and why.
Common problems
| Symptom | Probable cause | Fix |
|---|---|---|
404 on /.well-known/agentic-profile.json |
Host strips dotfiles, or your build pipeline didn't include them. | For Apache: add Require all granted on the directory. For Jekyll: add include: [.well-known]. For Eleventy: addPassthroughCopy. For everything else: use the Cloudflare Worker recipe. |
Returns 200 but with Content-Type: text/html |
Host's default for unknown extensions, or it's wrapping JSON in an HTML page. | Set the header explicitly (Nginx default_type, Apache Header set, Vercel vercel.json, Worker response). |
| Submission says "schema invalid" | Field missing, wrong enum value, raw number where a band is required. | Run pitch-mcp validate https://yourdomain.example locally - it'll point at the exact field. Author skill from the adopt page walks you through fixing it. |
| Embed mode: directory says "couldn't find a profile" | The <script> tag was filtered, or your CMS rewrote it. |
Open the live page, View Source, search for application/agentic-profile+json. If it's not there, your CMS stripped it - drop down to the Worker recipe or the XML fallback. |
| Submission says "domain not reachable" | SSRF guard rejected: private IP, malformed URL, or blocked port. | Make sure the URL is HTTPS (not HTTP), resolves to a public IP, and isn't behind a captive portal or VPN-only. |
Profile served but the directory's verified flag stays false |
No company.registry or company.lei. |
Add a Companies House / SEC / Delaware / state-of-incorporation entry, or a GLEIF LEI. Re-submit. Verification recomputes on every successful re-ingest. |
Still stuck? hello@agentic-first.co -
include the URL you're trying to publish from and the response
to the three curl commands above. We'll add a new
recipe to this page for any host worth covering that we
haven't already.