# @adland/data

## 0.21.0

### Minor Changes

- 759e960: `sponsoredContext` moves into this package.

  The schema, the topic list and the caps are the contract between whoever WRITES a sponsored context into a slot and whoever READS one, and those are not always the same program. It lived in the agent app, where every other reader had to copy it to agree with it.

  New exports: `sponsoredContextAd`, `sponsoredContextData`, `SponsoredContextData`, `SponsoredContextAd`, `parseSponsoredContext`, `SPONSORED_TOPICS`, `SponsoredTopic`, `sponsoredTopic`, `MAX_CONTEXT_CHARS`, `MAX_TOPICS`, `MAX_SPONSORS_PER_INFERENCE`.

  Additive. `adTypes` and `ads` are untouched, and deliberately: this ad renders nothing, so joining the union would oblige every exhaustive `Record<AdType, …>` — in this package, `@adland/react` and the web app — to answer "what label, what icon, where does a tap go" for a type with no answer, and would offer "sponsored context" among the things a buyer picks in the ad composer. It is exported alongside the union, not into it, so a consumer opts in by importing it and everything else stays unaware.

  It is built with `defineAd`, so it carries `process` / `safeProcess` like the rest. No `metadata` and no `getMetadata`: the others enrich because they point at something lookupable, and a sponsored context is already the whole payload.

  `context` is untrusted text that lands in a prompt. The schema bounds length and topics; it says nothing about content, and importing it does not make the text safe. The defence belongs at the prompt boundary.

- 9bcc91a: X posts go through X's own API, and an X account becomes an ad type.

  **The scraper is gone.** Posts were read from `cdn.syndication.twimg.com/tweet-result` — the endpoint an embedded post fetches to render itself — with a `token` mangled out of the post ID in base 36, a thirteen-item `features` string copied from the embed script, and a browser User-Agent. It worked, and every line of it was a guess about something nobody documented. X now sells per request rather than per month, so the guessing stopped being worth it: `@xdevplatform/xdk`, typed, sanctioned, against `api.x.com`.

  **Long posts were being truncated and nothing said so.** `text` caps at 280 characters and a longer post arrives already cut, with no flag in the response to mark it partial — so every long post advertised here lost its ending. The request now asks for `note_post`, whose `text` is the whole body. Its entities are a separate set and have to be: the offsets and shortlinks differ between the two texts, so expanding the truncated post's links against the full body puts back links that are not there and misses the ones that are.

  **A post's attachments are all kept.** A post carries up to four and only the first was stored, so a three-photo post was advertised as a one-photo post. `TweetMetadata` gains `media: XPostMedia[]` — type, still, dimensions and the author's alt text, in the order the post shows them. `image` stays beside it, redundant on purpose: every X ad already pinned to IPFS has that field and no `media`, and the renderers read it by name. On the ad's own schema `media` is optional rather than defaulted to `[]`, because an ad from before the field existed has no answer to the question and an empty array would claim the post had no pictures.

  **`xProfile` is a new ad type** — an account as the creative rather than something it posted. It stores a handle, not a numeric ID: the opposite choice from `farcasterProfile`, which keeps an `fid` because a Farcaster handle is rentable and an fid is forever. X has no stable public ID a person would recognise — nobody pastes their own numeric ID — and an ad whose data field is `1503851270` cannot be read by whoever placed it. A handle can change hands, and the stored metadata snapshot is what pins down who was meant.

  The metadata carries `followers`, which is the field this type exists for and the one no scrape could reach. It is a snapshot: it says what was true when the space was bought, not what is true now.

  `AdData` gains a member, so every exhaustive `Record<AdType, …>` needs an entry — minor rather than patch, which is what breaking means before 1.0.

  Advertising either X type now needs `X_API_BEARER_TOKEN` on the API. Without it the API still starts and every other ad type works; the two X types decline at the point of use, like every other optional integration.

## 0.20.0

### Minor Changes

- 7899811: Share where an ad points, name the advertiser, and put `placement` on the wire.

  `@adland/data` gains `adDestination`, `adHeadline` and `adTypeLabel`. The first
  was private to the web action adapter, which meant a server wanting to link to
  an ad had to reimplement "where does a cast go" — and a second copy of that is a
  second copy that can disagree. `WebActionAdapter` now reads the shared one.

  `@adland/config` turns `adChanged` into a function: the notification names the
  advertiser, the ad type and the creative's own first line instead of reading
  "Someone just took the space" every time. It clamps its own body, because that
  body is now part product copy and part a stranger's words — a Farcaster client
  truncates the END, which is exactly the half we wrote. `AD_PARAM` is added for
  pinning a link to one creative.

  `@adland/react` fixes the legacy `/track` beacon, which declared `placement` and
  `chainId` on `TrackingPayload`, took them from every caller, and serialised
  neither. Every unverified event landed with a null chain and a null placement;
  nothing failed, so nothing said so.

## 0.19.0

### Minor Changes

- f17898c: Carry a cast's attached picture, so a cast ad renders like an X post.

  The card has drawn a post's attachment for as long as posts have had cards, and
  an X post filled it while a cast never could: `castAd`'s metadata had no image
  field at all. The same layout therefore came out as a picture beside the words
  on one network and words alone on the other, for no reason a reader could see.

  - `castAd.metadata` gains `image`, named and shaped exactly as the tweet's.
  - `Cast.embeds` was typed `unknown[]`. Farcaster returns an OBJECT keyed by kind
    — `{ images, urls, videos, casts, … }` — so anything that read it found
    nothing and moved on without an error. Now describes the real shape.
  - New `castImage(cast)` picks the first attachment, preferring the still
    (`media.staticRaster`) over a source that may move. One definition, because
    the publish path and the card renderer both need this answer and a drift
    between them would show up as a card that quietly loses its picture.

## 0.18.0

### Minor Changes

- f8354d4: Token ads address a card carrying live market figures.

  `getAdCardKey` now returns `token/<chainId>/<address>` for a token ad, so
  `AdImage` draws a card with price, market cap, 24h volume, 24h change and
  liquidity instead of stretching the token's logo across the space. Chain first,
  because the same address is a different token on a different chain.

  `SupportedChain` gains `dexscreenerSlug`. A second id for the same chain because
  the two indexes answer different questions: CoinGecko knows a token's aggregate
  market cap, DexScreener knows a pool's liquidity and knows tokens deployed this
  week. Neither covers the other.

## 0.17.0

### Minor Changes

- fdd9fcd: Mini app ads now carry who published them.

  A manifest's account association is not only proof the domain is claimed — its
  header names the fid that claimed it. `miniappAd` metadata gains an optional
  `creator` with that fid resolved to a username, display name and picture, so an
  ad can credit a person rather than only a domain.

  Optional throughout, and null where it cannot be resolved: an app whose
  association will not decode, or whose author has since deleted their account,
  is still a perfectly good ad.

  Resolved through Farcaster's public API, so this adds no key requirement.

- 113b726: Token ads across four chains, identified by CAIP-10.

  A token address is meaningless without the chain it was minted on — the same
  twenty bytes are a different asset on every network. Token ads now accept
  Ethereum, Base, Arbitrum and Robinhood Chain, identified as
  `eip155:<chain>:0x…`, with `parseCaip10` / `toCaip10` and the supported-chain
  registry exported from `@adland/data`.

  Stored ads keep their `{ address, chainId }` shape, so nothing published before
  this needs migrating and every existing token ad keeps verifying. CAIP-10 is
  the notation callers pass and people read.

  `@adland/chains` adds `tokenChains`, and `getChainClient` takes an optional
  chain list. A slot only exists where it was deployed; a token exists wherever
  it was minted, and the two sets should not be the same list.

  Also fixes an override that never applied. `adlandApiUrl` read
  `NEXT_PUBLIC_ADLAND_API_URL` off a captured `process.env`, which no bundler can
  statically replace — so every browser build ignored the setting and talked to
  adland's production API regardless of configuration. It is now read as the
  literal member expression bundlers look for.

- e5764db: Advertise a post on X, and render posts as posts.

  `tweet` joins the ad types: paste an `x.com/…/status/…` link and it verifies
  through X's own syndication endpoint — the one an embedded post uses — so it
  needs no API key, no app, and no approval. What it yields is what a card shows:
  the author's name, handle and avatar, the text with t.co links put back to where
  they actually go, the date, and a still of any attachment. Not the whole post —
  threads, quotes and video are not reproduced — because the ad points at the
  post rather than replacing it. The permalink is rebuilt from the post itself, so
  a paste carrying `?s=20` or a `/photo/1` suffix still stores the canonical URL.

  A cast's metadata now carries `displayName`, and its `pfpUrl` works again: it
  was reading `author.pfp_url` and `timestamp` as a string, which are Neynar's
  names for fields the public Farcaster API returns as `author.pfp.url` and epoch
  milliseconds — so every cast ad had been quietly losing its author's picture
  since that migration.

  Both ad types store the same shape, because both are somebody saying something
  somewhere. New in `@adland/react` to lay one out:

  - `getAdPost(data)` — text, name, handle, avatar, time and the post's own
    media, or null for anything that is not a post. Keeps the avatar out of the
    media field, which the flat image accessors cannot do.
  - `<AdHandle>` and `<AdTimeAgo>` — the handle and a compact age (`3h`, then a
    date past a week), rendering nothing for ad types that have neither.
  - `parseAdTimestamp` / `formatTimeAgo` — the same reading, for callers not
    using the components. Both timestamp conventions are accepted, so ads
    published before this keep their dates.
  - `XLogo` — lucide retired its Twitter bird and never replaced it, and its `X`
    is the close button.

  `adCardLabel` moves to its own module and is now exported from
  `@adland/react/core` as well. The names are data; only the icons beside them
  need React, and a server route or edge function wanting to say "X post" should
  not have to pull the component layer in to do it.

  The mini app's share card is rebuilt on top of this: a post is laid out as a
  post — author, handle, date, then the words — rather than having the author's
  name promoted to a headline and what they said demoted to a caption.

### Patch Changes

- b665a59: Make clicks on a Farcaster ad survive a host that says no.

  Every mini app SDK action returns a promise, but the adapter routed clicks
  through a synchronous `try/catch`, which can only catch a synchronous throw. A
  host rejecting — an unsupported chain, a dismissed sheet, an older client
  missing the action — became an unhandled rejection, and the web fallback beneath
  the catch never ran in the cases it exists for. The adapter now awaits each
  action.

  Token ads fall back from `swapToken` to `viewToken` when a swap cannot be
  routed. Swap support covers fewer chains than tokens are minted on, and that
  gap widened when token ads gained Ethereum, Arbitrum and Robinhood — a click
  that cannot be swapped now shows the token instead of doing nothing.

  Adds `toCaip19` to `@adland/data`, so one place owns the difference between an
  asset id (`eip155:8453/erc20:0x…`, what the SDK wants) and the account form
  (`eip155:8453:0x…`) used elsewhere for the same token. Pinned by a test to the
  example in the SDK's own type documentation.

## 0.16.3

### Patch Changes

- 859ebec: Stop `getDomainManifest` throwing on domains with no cached manifest.

  Farcaster answers most domains with `{ verified: false, hasCachedManifest: false }` and no `manifest` field. That was parsed unconditionally, so every such domain produced `SyntaxError: "undefined" is not valid JSON` as an unhandled rejection. `manifest` is now `null` for both the uncached and unparseable cases, and the debug `console.log` of every API response is gone.

## 0.16.2

### Patch Changes

- b32cc89: Publisher SDK: platform-agnostic config, action adapters, and a framework-free web component.

  - **@adland/react**: optional `AdlandProvider` supplying `endpoint`/`gateway`/`rpcUrl`/`chainId`/`consent`/`onEvent` defaults (all overridable per-`Ad`); stable `AdEvent` shape mirrored to a publisher `onEvent`; consent-gated analytics beacon (default `granted`); `context` → `placement` with `context` kept as a deprecated alias. New `ActionAdapter` seam with `WebActionAdapter`/`FarcasterActionAdapter` (default preserves current behavior); injectable viem chain registry + `subgraphUrl`. New React-free `./core` and `./farcaster` subpath exports.
  - **@adland/embed** (new): `<adland-slot>` custom element with `render-mode` `isolated` (sandboxed iframe, default) and `inline` (shadow DOM with `::part()` hooks), CSS custom-property theming, and impression/click tracking. Dual self-contained CDN bundles — `adland.js` (web, zero Farcaster) and `adland-farcaster.js` (Mini App). Copy-paste simple by default, deeply customizable when wanted.
  - **@adland/data**: guard `process.env` access so the module is safe when bundled into browser builds.

## 0.16.1

### Patch Changes

- f53f495: Update production API URL from placeholder to real deployed domain. The default production API URL now points to `https://api.adland.space` instead of the previous placeholder. This can still be overridden via the `NEXT_PUBLIC_ADLAND_API_URL` environment variable.

## 0.16.0

### Minor Changes

- be2dab3: Split out of the 0xSlots monorepo into a standalone repository.

  **@adland/data**

  - `Cast` is no longer re-exported from `@neynar/nodejs-sdk`; a locally-owned
    `Cast` type is exported instead. This fixes a phantom dependency that broke
    standalone installs.
  - `decodeBase64Url` no longer uses Node's `Buffer`, so account-association
    parsing works in the browser.
  - `adlandApiUrl` can be overridden with `NEXT_PUBLIC_ADLAND_API_URL`.
  - `validateAdData(null)` and `validateAdData(undefined)` previously threw an
    uncaught `TypeError`. They now return
    `{ success: false, error: "Data does not match any known ad type" }`.

  **@adland/react**

  - **Breaking:** `@0xslots/sdk` is now a peer dependency. Consumers must install
    it directly. This prevents two copies of the SDK being linked into a bundle,
    which broke `SlotsChain` identity comparisons.
  - IPFS metadata now resolves through the econome gateway
    (`https://ipfs-gateway.econome.studio`) instead of a Pinata dedicated gateway.
    Override with the new `gateway` prop on `<Ad>`. There is no fallback gateway.
  - `fetchAdFromURI(uri, gateway?)` takes an optional second argument.
