af agentic-first

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.

Decision tree Static hosts CMS hosts Constrained hosts Author skills
On this page
  1. Pick the right mode (decision tree)
  2. File-host recipes (canonical mode)
  3. CMS-embed recipes (data-island mode)
  4. Constrained-host recipes (inline-XML fallback)
  5. Verify your profile is reachable
  6. Submit to the directory
  7. Common problems

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.

ModeWhere the profile livesUse it ifTrade-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

  1. Upload the file to your bucket at .well-known/agentic-profile.json.
  2. Set the object's Content-Type metadata to application/json (S3 infers this from the extension on most upload paths, but the AWS CLI --content-type flag 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"
  3. If CloudFront is in front of S3, you don't need a behaviour-level override; the Content-Type from the origin passes through. If you have a custom error page on 404, double-check it doesn't catch /.well-known/agentic-profile.json before 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

GeneratorFile pathNotes
Astropublic/.well-known/agentic-profile.jsonServed as-is. No config needed.
Next.jspublic/.well-known/agentic-profile.jsonServed as-is. For dynamic profile, use app/.well-known/agentic-profile.json/route.ts.
SvelteKitstatic/.well-known/agentic-profile.jsonServed as-is. Adapter-static + adapter-vercel both work.
Nuxtpublic/.well-known/agentic-profile.jsonServed as-is. (Nuxt 3.)
Hugostatic/.well-known/agentic-profile.jsonHugo copies static/ verbatim; dotfiles included by default.
JekyllRepo root .well-known/agentic-profile.json + add include: [.well-known] to _config.ymlJekyll excludes dotfiles by default - the include line is required.
Eleventysrc/.well-known/agentic-profile.json + passthrough copyAdd eleventyConfig.addPassthroughCopy(".well-known") to .eleventy.js.
Gatsbystatic/.well-known/agentic-profile.jsonServed as-is.
Docusaurusstatic/.well-known/agentic-profile.jsonServed as-is.
Astro (multipage build)public/.well-known/agentic-profile.jsonSame 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:

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

SymptomProbable causeFix
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.

Author skills (write the JSON) Reader skills (consume profiles) The standard Security details