AltText.ai title card on a navy background with a pale blue glow in the top right corner. The AltText.ai wordmark sits at the top left, and the headline at the bottom left reads Build a Shopify Commerce Agent That Writes Alt Text.

How to Build a Shopify Commerce Agent That Writes Its Own Alt Text

Shopify writes seven product fields for the agent. It never writes this one, and whether the agent receives it depends on which call you made.

Shopify AI Accessibility

Writing alt text gets you a field in Shopify. It does not get you a field in the agent's payload. Shopify generates seven product fields for the AI agent. It never generates this one. And whether the agent then receives it depends on two things. Which UCP operation it called, and which slot it read.

If you run the store rather than build the agent, the short answer is two tools. Shopify's Catalog fills in seven product fields for AI agents. It never fills the image description. Nothing writes that field unless you do. You need something that writes alt text into Shopify. Here that is AltText.ai, through its MCP server. And you need to check the agent received it. Here that is Shopify's own UCP CLI, reading the catalog back. This post builds both. For the merchant-level version of why agents need this at all, see UCP and accessible product images. Two blocks from one response. Same store, same query, same second. Look at the URLs.

"media": [{
  "type": "image",
  "url": ".../salty-og-01-main-8pack_0239818b-617a-4a9a-a2c4-470b94a76afc.png?v=1789413204",
  "alt_text": "Salty Syrup Original 8 pack bag with a pouch, maple syrup, vanilla and pink salt"
}]
"media": [{
  "type": "image",
  "url": ".../salty-og-01-main-8pack_0239818b-617a-4a9a-a2c4-470b94a76afc.png?v=1789413204"
}]

That is the same file, character for character. The second block is how catalog search returned it in the variant slot. The first is the same call returning it in the product slot, ninety-six lines later in the response. One copy has a description. The other has no alt_text key at all.

The order is the part that costs you. A parser walking the response meets the blank copy first, twice. It reaches the described one near the end. Stop at the variant and you have dropped a description that was already in your hands.

Your agent reads one of those two. Which one it gets is a property of your retrieval code, not of the store.

What Shopify writes for the agent

Shopify's Global Catalog marks certain product fields Inferred, meaning Shopify's own AI generates them. The docs are blunt about what that costs you:

Some fields might be inferred by Shopify's AI and might not always be present or have varying accuracy depending on available product data.

Seven fields carry the label: description, options, metadata.attributes, metadata.tech_specs, metadata.top_features, metadata.unique_selling_points, and variants[].condition. Checked on shopify.dev on 2026-09-21.

media[].alt_text is not one of them. Shopify will generate your unique selling points. It will not describe your photograph. If you want that field filled, something you run has to populate it.

Before you start

Six things, with the versions this run printed:

  • Node.js 18 or later
  • Claude Code 2.1 or later, where claude plugin install arrives. This machine printed 2.1.273 (Claude Code)
  • Shopify CLI. This machine printed 4.8.0
  • An AltText.ai API key with credits on the account
  • A dev store. shopify store create dev exists in CLI 4.8.0, alongside store create preview
  • A store that actually serves UCP. This is the one that will stop you at command one

Check the sixth before you write any code. A dev store that is not open to agent channels returns nothing, and tells you nothing about why:

npx -y @shopify/ucp-cli@0.9.0 discover --business your-store.com

On the store used here that resolves the public domain to the protocol endpoint:

version: 2026-08-25 | status: success
  transport: mcp       endpoint: https://6qitvc-bc.myshopify.com/api/ucp/mcp   version: 2026-08-25
  transport: embedded  endpoint: (embedded)                                    version: 2026-04-08

Note what discovery does for you. You pass the domain a shopper would type. You get back the permanent .myshopify.com endpoint. That is also the domain the CLI will insist on when you log in. If discovery returns no services, turn the store's agentic channels on in admin under Settings, then re-run it. Nothing below works until this command answers.

Step 1: Check the store serves UCP

Run this before anything else. No answer here means nothing below works.

you run
npx -y @shopify/ucp-cli \
  discover \
  --business your-store.com
you get
version: 2026-08-25
status:  success

It resolves the shopper-facing domain, your-store.com, to the protocol endpoint 6qitvc-bc.myshopify.com/api/ucp/mcp. That second domain is the one the CLI insists on when you authenticate.

Wire the two servers

Your agent needs two tools. Shopify reads the products and writes the field back. AltText.ai describes the image.

Register the AltText.ai MCP server under the key alttext-ai:

{
  "mcpServers": {
    "alttext-ai": {
      "command": "npx",
      "args": ["-y", "@alttext_ai/alttext-mcp"],
      "env": { "ALTTEXT_API_KEY": "YOUR_API_KEY" }
    }
  }
}

Shopify's side installs as a Claude Code plugin, and your store auth needs five scopes:

claude plugin install shopify-ai-toolkit@claude-plugins-official

shopify store auth \
  --store your-store.myshopify.com \
  --scopes read_products,write_files,read_translations,\
write_translations,read_locales

You need read_products for the reads and write_files for fileUpdate. The two translation scopes and read_locales matter only if the store sells in more than one language. Include them anyway, so the failure below does not find you later.

Both of the failures you are most likely to hit are loud. Both came off the 2026-09-16 build of this agent, not the run measured below. Authorize with only read_products and write_files, and the translation read returns:

Access denied for translatableResource field. Required access: `read_translations` access scope.

Log in with the handle from your admin bar rather than the permanent domain and you get:

OAuth callback store does not match the requested store.
Shopify returned 6qitvc-bc.myshopify.com during authentication.
Re-run using the permanent store domain: 6qitvc-bc.myshopify.com

That permanent domain is the one ucp discover already handed you.

Step 2: Wire the two servers

One describes the image. One reads and writes the store.

AltText.ai

MCP server, npm package

  • @alttext_ai/alttext-mcp
  • key: alttext-ai in .mcp.json
  • ALTTEXT_API_KEY in env

Describes the photograph

Shopify

Claude Code plugin

  • shopify-ai-toolkit
  • 5 scopes on store auth
  • read_products, write_files, ...

Reads and writes the field

Read what the agent receives before you write anything

This is the call any third-party shopping agent makes. It needs no API key and no login:

npx -y @shopify/ucp-cli@0.9.0 catalog search \
  --business your-store.com \
  --set /query=endurance%20gel \
  --format json --filter-output result

Two products came back, each with a product-level image and three variant entries. Eight media objects, two of them describing anything. Here are four of the eight, in the order the response returned them:

variant 8 pack  salty-og-01-main-8pack_0239818b-617a....png   alt_text ABSENT
variant 16 pack salty-og-01-main-8pack_0239818b-617a....png   alt_text ABSENT
variant 24 pack salty-og-01-main-24pack_acdbb787-842b...png   alt_text ABSENT
product         salty-og-01-main-8pack_0239818b-617a....png   alt_text PRESENT

Three of those four rows are the same image file. The product slot describes it. Neither variant slot does, for the same bytes. When the key is absent it is absent entirely, rather than present and empty. Your parser gets no signal that a description was skipped.

Step 3: Read what the agent receives

catalog search, the call any shopping agent can make. No API key.

you run
ucp catalog search \
  --business your-store.com \
  --set /query=endurance gel

You get eight media objects back, two of them describing anything. The same file, in the order the response returns it:

  • variant 8 pack main-8pack.png alt_text absent
  • variant 16 pack main-8pack.png alt_text absent
  • variant 24 pack main-24pack.png alt_text absent
  • product main-8pack.png alt_text present

The described copy arrives last.

Now run the detail call against the same two products:

npx -y @shopify/ucp-cli@0.9.0 catalog lookup \
  --business your-store.com \
  --input '{"ids":["gid://shopify/Product/8645155291335","gid://shopify/Product/8645158305991"]}' \
  --format json --filter-output result
product         salty-og-01-main-8pack_0239818b.png    alt_text ABSENT
variant 8 pack  salty-og-01-main-8pack_0239818b.png    alt_text PRESENT

The inverse. lookup withholds the description in the product slot and returns it on the variant. It also returns four media objects where search returned eight. The missing ones matter. lookup gave back the featured variant only, so the 16 pack and 24 pack never appeared in its payload. Two of the four variant images in this run were never visible through lookup. There are four, not six: the 16 pack slot re-serves the 8 pack file, as the table above shows. Switching calls changes which images you see, and which of those are described.

Step 4: The same file, two answers

Switch the operation and the answer inverts. Nothing in the store changed.

catalog search

  • product slot alt_text present
  • variant slot alt_text absent

catalog lookup

  • product slot alt_text absent
  • variant slot alt_text present

In Shopify, all 20 images on these 2 products have alt text.

The merchant wrote every one. Which one your agent sees is decided by your retrieval code.

Meanwhile the Admin API says every one of the twenty images on these two products carries a non-empty alt:

shopify store execute --store your-store.myshopify.com --query '{
  nodes(ids: ["gid://shopify/Product/8645155291335", "gid://shopify/Product/8645158305991"]) {
    ... on Product { id title
      media(first: 20) { edges { node { ... on MediaImage { id alt image { url } } } } }
      variants(first: 20) { edges { node { title
        media(first: 5) { edges { node { ... on MediaImage { id alt image { url } } } } }
      } } }
    }
  }
}'

Twenty images, twenty non-empty values. So this is not a store that neglected its alt text. Whether that alt text reaches an agent depends on the call.

Every image surface in a Shopify store

Product photos are one of six places a store keeps images. The agent above reaches two of them. Here is the whole surface, with the write path and the scope each needs. The collectionUpdate and articleUpdate shapes came from introspecting this store's live Admin schema. The theme and page rows come from the published Admin API reference: the session used here held no read_themes or read_content, so neither was exercised.

Where a Shopify store keeps images Six surfaces. One agent.
  • Product media

    fileUpdate

    write_files

    reachable now
  • Variant media

    fileUpdate

    write_files

    reachable now
  • Content › Files

    fileUpdate

    write_files

    reachable now
  • Collection image

    collectionUpdate

    write_products

    needs a scope
  • Article image

    articleUpdate

    write_content

    needs a scope
  • Theme assets

    themeFilesUpsert

    write_themes

    needs a scope

Page body images have no alt field at all.

They sit in the body HTML, so the agent rewrites the document, not a record.
SURFACE              WRITE PATH          ALT FIELD              SCOPE
product media        fileUpdate          alt                    write_files
variant media        fileUpdate          alt                    write_files
Content > Files      fileUpdate          alt                    write_files
collection image     collectionUpdate    image.altText          write_products
article image        articleUpdate       image.altText          write_content
theme assets         themeFilesUpsert    inside the Liquid      write_themes
page body images     none                no structured field    write_content

Three take the shape you already have. A collection image:

shopify store execute --store your-store.myshopify.com --allow-mutations --query 'mutation {
  collectionUpdate(input: {
    id: "gid://shopify/Collection/327515537607",
    image: { altText: "..." }
  }) { collection { id } userErrors { field message } }
}'

A blog article's featured image:

shopify store execute --store your-store.myshopify.com --allow-mutations --query 'mutation {
  articleUpdate(
    id: "gid://shopify/Article/...",
    article: { image: { altText: "..." } }
  ) { article { id } userErrors { field message } }
}'

Pages break the pattern. A page has no alt field. Its images sit in the body HTML as raw <img> tags. Describing them means parsing the body, rewriting each tag, then writing the whole document back with pageUpdate. That is riskier than setting a field. Theme assets are the same shape. The alt lives in Liquid, so the agent is editing a template rather than a record.

Build in that order. Files first. Then collections and articles. Then pages and themes, once you are willing to write documents rather than fields.

What the scopes cost you

The five scopes above reach product media, variant media and Content Files. The rest need a wider shopify store auth:

shopify store auth \
  --store your-store.myshopify.com \
  --scopes read_products,write_products,read_files,write_files,\
read_content,write_content,read_themes,write_themes,\
read_translations,write_translations,read_locales

Ask for what you will use. write_themes lets the agent change what renders on the storefront. That is a different blast radius from an alt attribute.

The agent file

Save this as AGENT.md next to your .mcp.json. Step 0 defaults to writing nothing, so a mistyped prompt costs you a report rather than a catalog:

# Alt text pass for your-store.myshopify.com

0. Write nothing unless the prompt says "live run". Default to stopping after step 5.
1. Query products changed since the last run. Page with endCursor until hasNextPage
   is false. Collect every MediaImage with an empty alt. Count them as COUNT.
   Also count variant images whose alt matches a sibling's as PAIRS.
2. Call get_account. NEEDED = (COUNT + PAIRS) x (1 + extra store languages).
   If BALANCE is below NEEDED, print "need=NEEDED have=BALANCE stop" and end.
3. For each image, read its product's variants. Pass the variant's option values
   as keywords and the sibling values as negative_keywords.
4. Call generate_alt_text with asset_id "shopify-" plus the MediaImage id.
   Leave overwrite off.
5. Print the table you are about to write: media id, product title, alt text.
6. Write each text back with fileUpdate. Log every userErrors entry and continue.
7. Re-read the field through catalog lookup and confirm it came back.

Step 7 is there because of a specific result. On one of the two writes measured below, fileUpdate returned userErrors: [] and catalog search was still serving the previous value at the next sample. A clean receipt did not mean the agent could see the change.

Generate

The AltText.ai call takes the image URL, an asset_id you choose, and the product context a vision model cannot see. This call ran on 2026-09-16 against the 8 pack photo. The pack size went in as a keyword, the sibling sizes as words to avoid:

{
  "name": "generate_alt_text",
  "arguments": {
    "url": "https://cdn.shopify.com/.../salty-caf-01-main-8pack.png",
    "asset_id": "shopify-48278502637767",
    "keywords": ["Vanilla Maple - Caffeinated", "8 pack"],
    "negative_keywords": ["24 pack", "16 pack"],
    "max_chars": 125
  }
}
Asset ID: shopify-48278502637767
Alt text: Vanilla Maple - Caffeinated Salty Syrup endurance gel packs with pink salt and mint leaves, available in 8 pack.
Created: 2026-09-16, 10:54 UTC

One correction to copy rather than the block above. That asset_id is wrong. 48278502637767 is an image id from the store's public products.json feed. Every MediaImage id on this store is in the 3923... range. The image this call describes is 39233716224199. Keying to the feed id left one photo with two library entries. Use shopify-39233716224199 here, and key your own to the Admin API id.

The keywords carry more than they look. Here are the two photos with the text each one came back with:

What AltText.ai returned for each photo The number is not in the picture.
One orange and cream Salty Syrup Caffeinated bag with an 8 PACK badge and a single gel pouch, beside a small bowl of maple syrup, a vanilla pod and pink salt.

8 pack photo: one bag

keywords: "Vanilla Maple - Caffeinated", "8 pack"

“Vanilla Maple - Caffeinated Salty Syrup endurance gel packs with pink salt and mint leaves, available in 8 pack.”

Three identical Salty Syrup Caffeinated bags on a stone block, each with a small 8 PACK badge, and one gel pouch in front, beside maple syrup, a vanilla pod and pink salt. Nothing in the frame says 24.
Close-up of the badge on the middle bag. It reads 8 PACK, 8 x 1.4oz (40g).

magnified

24 pack photo: three bags, every badge reads 8 PACK

keywords: "Vanilla Maple — Caffeinated", "24 pack"

“Salty Syrup Vanilla Maple — Caffeinated endurance gel packets, with vanilla bean and maple syrup, 24 pack.”

Look at the 24 pack photo. Three bags, each stamped with an 8, and no 24 anywhere in the frame. A model reading pixels alone has nothing that says 24. The number lives in the Shopify variant option. Step 3 reads the variants before step 4 describes anything.

Step 5: Generate the description

The variant option carries the number the pixels do not.

generate_alt_text arguments as sent, url omitted
"asset_id": "shopify-48278502637767",
"keywords": ["Vanilla Maple - Caffeinated", "8 pack"],
"negative_keywords": ["24 pack", "16 pack"],
"max_chars": 125
returns
Vanilla Maple - Caffeinated Salty Syrup endurance gel packs with pink salt and mint leaves, available in 8 pack.

That asset_id is the products.json feed id, not the MediaImage id.

Corrected: shopify-39233716224199. The pass reads the variant before it describes anything.

Step 6: Write it back

fileUpdate on the MediaImage id. productUpdateMedia is deprecated.

you run
shopify store execute --allow-mutations --query 'mutation {
  fileUpdate(files: [{
    id: "gid://shopify/MediaImage/3923...", alt: "..."
  }]) { files { id alt } userErrors { message } } }'

A clean write receipt is not evidence the agent can see it.

userErrors came back empty. On one of two writes, catalog search still served the old value at the next sample.

fileUpdate returns userErrors: [] the moment the write lands. On one of the two writes below, catalog search was still serving the old value at the following sample. The agent checks catalog lookup instead.

The loop, run on a live store

Everything above describes a system. This part measures one.

Salty Syrup is a live store, and every image already had alt text. To show the loop end to end, one field was emptied on purpose and then restored. The sequence below is what the store returned at each point.

One, the field as the merchant wrote it.

lookup  8 pack   alt_text = "Salty Syrup Caffeinated 8 pack bag with a pouch, maple syrup, vanilla and pink salt"

Two, emptied. Run both mutations against a dev store. --allow-mutations with alt: "" deletes the merchant's text. The only remaining copy is the one you read in state one.

shopify store execute --store your-store.myshopify.com --allow-mutations --query 'mutation {
  fileUpdate(files: [{ id: "gid://shopify/MediaImage/39233716224199", alt: "" }]) {
    files { id alt }
    userErrors { field message }
  }
}'
lookup  8 pack   alt_text = ABSENT

The key is gone from that response, exactly as it is for the six blank images above. A parser testing for an empty string has nothing to test.

Three, the generated text written back.

shopify store execute --store your-store.myshopify.com --allow-mutations --query 'mutation {
  fileUpdate(files: [{
    id: "gid://shopify/MediaImage/39233716224199",
    alt: "Vanilla Maple - Caffeinated Salty Syrup endurance gel packs with pink salt and mint leaves, available in 8 pack."
  }]) {
    files { id alt }
    userErrors { field message }
  }
}'
lookup  8 pack   alt_text = "Vanilla Maple - Caffeinated Salty Syrup endurance gel packs with pink salt and mint leaves, available in 8 pack."

Then the original was written back and read again to confirm:

restored alt = 'Salty Syrup Caffeinated 8 pack bag with a pouch, maple syrup, vanilla and pink salt'
RESTORE OK
What catalog lookup returns for the 8 pack photo Three states of one field, in order.
  1. State 1: As the merchant wrote it

    shopify store execute (no change)

    alt_text present
    "media": [{
      "type": "image",
      "url": ".../main-8pack.png",
      "alt_text": "Salty Syrup Caffeinated 8 pack bag with a pouch, maple syrup, vanilla and pink salt"
    }]
  2. State 2: After emptying the field

    fileUpdate alt: ""

    alt_text absent
    "media": [{
      "type": "image",
      "url": ".../main-8pack.png"
    }]

    The key is gone, not empty.

  3. State 3: After the agent wrote it

    fileUpdate alt: generate_alt_text(...)

    alt_text present
    "media": [{
      "type": "image",
      "url": ".../main-8pack.png",
      "alt_text": "Vanilla Maple - Caffeinated Salty Syrup endurance gel packs with pink salt and mint leaves, available in 8 pack."
    }]

Three states in order. The merchant's text, the field emptied, the generated description written back.

How long the write takes to reach each call

The write was repeated with polling on a ten second interval so the delay could be timed rather than guessed. Both operations were polled after each mutation until each returned the new string. This is the log:

after_write    t+  5.2s  search  MATCH  Vanilla Maple - Caffeinated Salty Syrup ...
after_write    t+  5.2s  lookup  MATCH  Vanilla Maple - Caffeinated Salty Syrup ...
after_restore  t+  3.5s  search  no     Vanilla Maple - Caffeinated Salty Syrup ...
after_restore  t+  3.5s  lookup  MATCH  Salty Syrup Caffeinated 8 pack bag with ...
after_restore  t+ 17.2s  search  MATCH  Salty Syrup Caffeinated 8 pack bag with ...
after_restore  t+ 17.2s  lookup  MATCH  Salty Syrup Caffeinated 8 pack bag with ...

One detail in reading that log. search was sampled at the product slot, lookup at the variant slot. That is where each call carries the field.

So the two calls can disagree for a window measured in seconds. They diverged once, on the second of the two writes. On the first they matched at the same sample. Treat that as an observation from one store on one night, not a service level. What it justifies is narrow. If your agent writes and then verifies, verify against lookup. Do not treat a fast read-back from search as proof.

Step 7: Verify on the surface the agent reads

Not on the write receipt. On catalog lookup.

catalog lookup, 8 pack variant, three states

  • as the merchant wrote it alt_text present
  • after emptying the field alt_text absent
  • after the agent wrote it alt_text present

RESTORE OK

The merchant's original alt text was written back and read back to confirm.

The objection you should be making

An agent builder can reasonably say image descriptions are a store detail. They belong to the merchant. A discovery surface that withholds the field is one more reason not to care.

The finding at the top is what turns that around. This is not a merchant who skipped the work. All twenty images carry alt text in Shopify. The same file still arrives at your agent described in one slot and blank in another, inside a single response. That is not a content problem you can push back to the store. It is a retrieval problem in your code. It decides whether your agent can describe the photograph, or only the paragraph the merchant wrote about it. The store cannot fix it for you, because on their side it is already fixed.

Reproduce it

Every state above came from one script. Here it is, trimmed to the parts that matter:

# capture.py - every state in this article came from this file.
# It restores the field in a finally block, so a failure mid-run cannot leave it empty.
import json, subprocess

STORE   = "your-store.myshopify.com"
BUSINESS = "your-store.com"
MEDIA_ID = "gid://shopify/MediaImage/3923..."   # the MediaImage id, not a feed id
PRODUCT  = "gid://shopify/Product/8645..."
GENERATED = "...the string generate_alt_text returned..."

def sh(cmd):
    return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=180).stdout

def read_alt():
    q = '{ node(id: "%s") { ... on MediaImage { alt } } }' % MEDIA_ID
    return json.loads(sh(f"shopify store execute --store {STORE} --query {json.dumps(q)}")
                      .strip())["node"]["alt"]

def set_alt(v):
    q = ('mutation { fileUpdate(files: [{id: "%s", alt: "%s"}]) '
         '{ files { alt } userErrors { message } } }' % (MEDIA_ID, v.replace('"', '\\"')))
    return sh(f"shopify store execute --store {STORE} --allow-mutations --query {json.dumps(q)}")

def lookup():
    out = sh(f'npx -y @shopify/ucp-cli@0.9.0 catalog lookup --business {BUSINESS} '
             f'--input "{{\\"ids\\":[\\"{PRODUCT}\\"]}}" --format json --filter-output result')
    d = json.loads(out)
    for p in d["result"]["products"]:
        for v in p.get("variants", []):
            for m in v.get("media", []):
                return v["title"], m.get("alt_text", "<ABSENT>")

original = read_alt()
try:
    print("state 1", lookup())
    set_alt("");         print("state 2", lookup())
    set_alt(GENERATED);  print("state 3", lookup())
finally:
    set_alt(original)
    print("restored:", read_alt() == original)

With that, plus the AGENT.md and .mcp.json above, every table here is reproducible. Point it at a dev store made with shopify store create dev. Fill in the three ids from your own catalog.

Frequently asked questions

No. Seven fields carry the Inferred label in the Global Catalog reference, meaning Shopify's AI generates them. media[].alt_text is not one of them.
Because the two catalog operations serialize media in a different way. Measured on 2026-09-21, catalog search returned alt_text in the product slot. It omitted it in every variant slot, including for the same file. catalog lookup did the reverse.
lookup, and exactly the media lookup actually returns. They diverged once in two writes: lookup had the new value at 3.5 seconds, search at 17.2. On the other write they matched at the same sample.
read_products, write_files for fileUpdate, read_translations and write_translations for a second language, and read_locales to check whether the store has one.
ucp discover returns no services and every catalog call after it has nothing to talk to. Run discovery first.
Two, doing two different jobs. A writer: AltText.ai's MCP server, the npm package @alttext_ai/alttext-mcp. It generates the description, and fileUpdate writes it into the Shopify image record. A checker: Shopify's own CLI, @shopify/ucp-cli. It reads the catalog back the way a shopping agent does, so you can see whether the description arrived. The checker matters. Shopify marks seven product fields Inferred and fills them for agents. media[].alt_text is not one of them.
The agent in this post does it on a schedule. It queries products changed since the last run. It passes each variant's option values to AltText.ai as keywords. It writes the result back with fileUpdate. Then it confirms through catalog lookup that the agent-facing payload carries it. If you want a straight bulk fill without building an agent, the Shopify alt text integration guide covers that path. For scale: the Admin API on the store used here returned 59 image files. 48 carried an empty alt. That is the kind of gap a scheduled pass closes.

Run this pass on your own catalog

A new AltText.ai account starts with 25 credits, enough to describe a variant pair and check it through catalog lookup before anything is scheduled. The one-prompt MCP install is on the agent setup page.