> ## Documentation Index
> Fetch the complete documentation index at: https://docs.context.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a recognizable merchant feed

> Turn merchant domains or transaction descriptors into readable transaction rows while preserving unresolved states and user corrections.

export const AgentSetupPrompt = ({children, variant = "setup"}) => {
  const isRecipe = variant === "recipe";
  const promptLabel = isRecipe ? "recipe prompt" : "setup prompt";
  const [copyState, setCopyState] = useState("idle");
  const contentRef = useRef(null);
  const detailsRef = useRef(null);
  const timerRef = useRef(null);
  const mountedRef = useRef(true);
  const pendingRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
      if (timerRef.current !== null) clearTimeout(timerRef.current);
    };
  }, []);
  const copyPrompt = useCallback(async () => {
    if (pendingRef.current) return;
    pendingRef.current = true;
    if (timerRef.current !== null) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }
    setCopyState("copying");
    try {
      const code = contentRef.current?.querySelector("pre code") ?? contentRef.current?.querySelector("pre");
      const prompt = code?.textContent;
      if (typeof prompt !== "string" || !prompt.trim()) {
        throw new Error("Prompt is not available.");
      }
      if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
        throw new Error("Clipboard access is not available.");
      }
      await navigator.clipboard.writeText(prompt);
      if (!mountedRef.current) return;
      setCopyState("copied");
      timerRef.current = setTimeout(() => {
        if (mountedRef.current) setCopyState("idle");
        timerRef.current = null;
      }, 4000);
    } catch {
      if (!mountedRef.current) return;
      if (detailsRef.current) detailsRef.current.open = true;
      setCopyState("error");
    } finally {
      pendingRef.current = false;
    }
  }, []);
  const feedback = copyState === "copied" ? `${isRecipe ? "Recipe" : "Setup"} prompt copied. Paste it into your coding agent.` : copyState === "error" ? "Couldn't copy automatically. The prompt is open below. Select and copy the text manually." : "";
  return <section className="agent-setup-prompt not-prose" aria-label={`Agent ${promptLabel}`}>
      <style>{`
        .agent-setup-prompt {
          --asp-surface: oklch(0.985 0.005 255);
          --asp-border: oklch(0.9 0.012 255);
          --asp-title: oklch(0.25 0.025 255);
          --asp-text: oklch(0.45 0.021 255);
          --asp-accent: oklch(0.49 0.2 264);
          --asp-accent-hover: oklch(0.44 0.19 264);
          --asp-on-accent: oklch(0.985 0.005 255);
          --asp-focus: oklch(0.6 0.19 255);
          margin: 1.5rem 0;
          border: 1px solid var(--asp-border);
          border-radius: 0.875rem;
          background: var(--asp-surface);
          color: var(--asp-title);
          font-family: inherit;
          min-width: 0;
        }
        .dark .agent-setup-prompt {
          --asp-surface: oklch(0.2 0.014 255);
          --asp-border: oklch(0.33 0.016 255);
          --asp-title: oklch(0.96 0.006 255);
          --asp-text: oklch(0.78 0.013 255);
          --asp-accent: oklch(0.73 0.15 255);
          --asp-accent-hover: oklch(0.8 0.11 255);
          --asp-on-accent: oklch(0.19 0.025 255);
          --asp-focus: oklch(0.78 0.14 255);
        }
        .agent-setup-prompt__header { padding: 1.125rem 1.25rem; }
        .agent-setup-prompt__row {
          display: flex;
          align-items: flex-start;
          justify-content: space-between;
          gap: 1.25rem;
        }
        .agent-setup-prompt__intro { min-width: 0; }
        .agent-setup-prompt__title {
          margin: 0;
          font-size: 1.125rem;
          font-weight: 650;
          line-height: 1.4;
          color: var(--asp-title);
        }
        .agent-setup-prompt__subtitle {
          max-width: 65ch;
          margin: 0.375rem 0 0;
          font-size: 0.875rem;
          line-height: 1.6;
          color: var(--asp-text);
        }
        .agent-setup-prompt__copy {
          display: inline-flex;
          flex-shrink: 0;
          align-items: center;
          justify-content: center;
          gap: 0.5rem;
          min-height: 2.75rem;
          padding: 0.625rem 0.875rem;
          border: 1px solid transparent;
          border-radius: 0.5rem;
          background: var(--asp-accent);
          color: var(--asp-on-accent);
          font: inherit;
          font-size: 0.8125rem;
          font-weight: 600;
          line-height: 1.4;
          cursor: pointer;
        }
        .agent-setup-prompt__copy:hover:not(:disabled) {
          background: var(--asp-accent-hover);
        }
        .agent-setup-prompt__copy:disabled { cursor: wait; }
        .agent-setup-prompt__copy:focus-visible,
        .agent-setup-prompt__summary:focus-visible {
          outline: 2px solid var(--asp-focus);
          outline-offset: 3px;
        }
        .agent-setup-prompt__feedback {
          margin: 0.75rem 0 0;
          font-size: 0.8125rem;
          line-height: 1.5;
          color: var(--asp-text);
        }
        .agent-setup-prompt__feedback:empty { margin: 0; }
        .agent-setup-prompt__details { border-top: 1px solid var(--asp-border); }
        .agent-setup-prompt__summary {
          display: flex;
          align-items: center;
          gap: 0.5rem;
          padding: 0.75rem 1.25rem;
          border-radius: 0 0 0.875rem 0.875rem;
          font-size: 0.8125rem;
          font-weight: 500;
          line-height: 1.5;
          color: var(--asp-text);
          list-style: none;
          cursor: pointer;
        }
        .agent-setup-prompt__summary::-webkit-details-marker { display: none; }
        .agent-setup-prompt__summary:hover { color: var(--asp-title); }
        .agent-setup-prompt__details[open] .agent-setup-prompt__chevron {
          transform: rotate(90deg);
        }
        .agent-setup-prompt__content { min-width: 0; padding: 0 1.25rem 1rem; }
        .agent-setup-prompt__content > :first-child { margin-top: 0; }
        .agent-setup-prompt__content > :last-child { margin-bottom: 0; }
        .agent-setup-prompt__content pre {
          max-height: 32rem;
          overflow: auto;
          white-space: pre-wrap;
          overflow-wrap: anywhere;
        }
        @media (max-width: 640px) {
          .agent-setup-prompt__header { padding: 1rem; }
          .agent-setup-prompt__row { flex-direction: column; gap: 0.875rem; }
          .agent-setup-prompt__copy { width: 100%; }
          .agent-setup-prompt__summary { padding: 0.75rem 1rem; }
          .agent-setup-prompt__content { padding: 0 1rem 1rem; }
        }
      `}</style>

      <div className="agent-setup-prompt__header">
        <div className="agent-setup-prompt__row">
          <div className="agent-setup-prompt__intro">
            <h2 className="agent-setup-prompt__title">
              {isRecipe ? "Have your agent implement this recipe" : "Set up Context.dev"}
            </h2>
            <p className="agent-setup-prompt__subtitle">
              {isRecipe ? "Copy this prompt into your coding agent to build the recipe in your project." : "Paste into your coding agent. It will ask about your goal and project before making changes."}
            </p>
          </div>
          <button className="agent-setup-prompt__copy" type="button" onClick={copyPrompt} disabled={copyState === "copying"} aria-busy={copyState === "copying"}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden="true" focusable="false">
              {copyState === "copied" ? <path d="m5 12 4 4L19 6" strokeLinecap="round" strokeLinejoin="round" /> : <>
                  <rect x="8" y="8" width="12" height="12" rx="2" />
                  <path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3" />
                </>}
            </svg>
            {copyState === "copied" ? "Copied" : copyState === "copying" ? "Copying…" : `Copy ${promptLabel}`}
          </button>
        </div>
        <p className="agent-setup-prompt__feedback" role="status" aria-live="polite" aria-atomic="true">
          {feedback}
        </p>
      </div>

      <details className="agent-setup-prompt__details" ref={detailsRef}>
        <summary className="agent-setup-prompt__summary">
          <svg className="agent-setup-prompt__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden="true" focusable="false">
            <path d="m9 5 7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          {`View ${promptLabel}`}
        </summary>
        <div className="agent-setup-prompt__content" ref={contentRef}>
          {children}
        </div>
      </details>
    </section>;
};

<AgentSetupPrompt variant="recipe">
  ```text Recipe prompt theme={null}
  Implement this recipe in my project:
  https://docs.context.dev/use-cases/merchant-transaction-feeds

  Read the recipe and linked API guides, inspect this project's stack, and build the complete flow using its existing conventions.

  Build transaction rows with merchant names and logos while preserving the original descriptor. Use Brand by domain when a verified domain is available, otherwise use transaction lookup with only the supplied hints. Include all matching hints in the cache key, expose unresolved and failed states, and preserve user corrections across refreshes. Do not invent confidence scores.

  Reuse existing Context.dev configuration and keep secret API keys on the server. Add focused tests, run the relevant checks, and document setup and how to try the result.
  ```
</AgentSetupPrompt>

Show a merchant name and logo next to a transaction while keeping the original statement descriptor available. Context.dev supplies a candidate Brand profile; your application decides whether to accept the match and how to display it.

[Spendify uses Context.dev for merchant branding](https://www.context.dev/blog/spendify-ships-clean-merchant-branding-with-context-dev) after resolving merchant domains in its own pipeline. If your data starts with raw descriptors, use the transaction lookup branch below.

## Choose the lookup from the data you have

```mermaid theme={null}
flowchart LR
  A[Transaction] --> B{Verified merchant domain?}
  B -->|Yes| C[Brand by domain]
  B -->|No| D[Brand by transaction and supplied hints]
  C --> E[Validate candidate]
  D --> E
  E --> F[Matched or unresolved row]
  F --> G[Apply saved user correction]
```

Use the [domain lookup guide](/guides/retrieve-brand-by-domain) or the [transaction enrichment guide](/guides/enrich-transaction-codes) for complete requests in cURL and every SDK. Both use `POST /brand/retrieve` on your server with a secret API key from the [Quickstart](/quickstart).

The following TypeScript adapter creates the request and an application cache key. Pass `request` as the body of the lookup. Only send hints that came from your payment processor or another verified source.

```typescript merchant-input.ts theme={null}
import { createHash } from "node:crypto";

type Transaction = {
  id: string;
  rawDescriptor: string;
  merchantDomain?: string;
  mcc?: string;
  country?: string;
  city?: string;
  phone?: string;
};

export function merchantLookup(tenantId: string, transaction: Transaction) {
  const domain = transaction.merchantDomain?.trim().toLowerCase();
  const request = domain
    ? { type: "by_domain" as const, domain }
    : {
        type: "by_transaction" as const,
        transaction_info: transaction.rawDescriptor,
        mcc: transaction.mcc,
        country_gl: transaction.country,
        city: transaction.city,
        phone: transaction.phone,
        high_confidence_only: true,
      };

  const normalize = (value?: string) => value?.trim().replace(/\s+/g, " ").toLowerCase() ?? null;
  const inputs = domain
    ? ["domain", domain]
    : ["transaction", normalize(transaction.rawDescriptor), transaction.mcc ?? null,
       normalize(transaction.country), normalize(transaction.city),
       normalize(transaction.phone), request.high_confidence_only];
  const cacheKey = createHash("sha256")
    .update(JSON.stringify([tenantId, ...inputs]))
    .digest("hex");

  return { request, cacheKey };
}
```

Validate the input before queuing it. `transaction_info` accepts 3 to 500 characters. Keep its original value in the request and ledger; normalization above is only for cache identity. Include every hint that can change the match, and scope the cache to your application's account boundary.

## Preserve the lookup outcome

Treat these states separately in your worker:

| Result                                            | Save                                                 | Next action                                         |
| ------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------- |
| Profile accepted after checking available context | `matched`, candidate profile, lookup time, input key | Render the candidate.                               |
| No profile or an ambiguous candidate              | `unresolved`, original descriptor                    | Show a neutral row and allow correction.            |
| Network failure, `408`, `429`, or server error    | `retryable_error`, last attempt time                 | Retry with bounded backoff and honor `Retry-After`. |
| Invalid input or authentication error             | `error`, diagnostic code                             | Correct the input or integration before retrying.   |

An HTTP success does not establish that the candidate is the right local merchant. Compare the domain and title with the available context, especially for payment processors, franchises, and parent brands. Keep a review state when your application cannot make that decision.

The Brand response has no public numeric confidence score. `high_confidence_only` is a request mode; do not turn it into a displayed percentage. Brand enrichment also does not provide a fraud verdict or legal-entity verification.

## Render the row with corrections first

Store corrections separately from cached API results. The following view model uses the correction whenever one exists, including after a failed refresh. `logoUrl: null` is an explicit choice to use no logo.

```typescript merchant-view.ts theme={null}
type Merchant = { name: string; domain: string | null; logoUrl: string | null };
type Lookup =
  | { status: "matched"; merchant: Merchant; retrievedAt: string }
  | { status: "unresolved" | "retryable_error" | "error"; attemptedAt: string };
type Correction = {
  merchant: Merchant;
  editedAt: string;
  editedBy: string;
};

export function transactionView(
  transaction: { id: string; rawDescriptor: string },
  lookup: Lookup,
  correction: Correction | null,
) {
  const merchant = correction?.merchant ??
    (lookup.status === "matched" ? lookup.merchant : null);

  return {
    transactionId: transaction.id,
    originalDescriptor: transaction.rawDescriptor,
    name: merchant?.name ?? transaction.rawDescriptor,
    domain: merchant?.domain ?? null,
    logoUrl: merchant?.logoUrl ?? null,
    source: correction ? "user" : merchant ? "context" : "statement",
    status: correction ? "corrected" : lookup.status,
  };
}
```

Display `originalDescriptor` in the transaction details. Use a fixed image box and a neutral fallback when a logo is missing or fails to load. Keep the amount, currency, booking date, and processor category from your ledger; a Brand profile should not overwrite those fields.

If a correction should apply to future transactions, ask the user to choose its scope and save a separate merchant rule. Do not silently apply a correction for one transaction to every similar descriptor.

## Refresh without losing decisions

Cache accepted candidates with the matching input key and retrieval time. Give unresolved results a shorter retry window; do not cache a temporary failure as a permanent miss. On refresh, replace only the automatic result, then apply the saved correction again.

A useful feed test includes four rows: a known merchant domain, a descriptor with supplied location hints, an unresolved descriptor, and a manually corrected merchant. Replay enrichment after a temporary failure and verify that the corrected row and original ledger data remain intact.

<CardGroup cols={2}>
  <Card title="Transaction lookup" icon="receipt" href="/guides/enrich-transaction-codes">
    Send descriptors and optional matching hints.
  </Card>

  <Card title="Company directory logos" icon="building" href="/use-cases/company-directory-logos">
    Add accessible image fallbacks and editable branding.
  </Card>
</CardGroup>
