---
name: merchant-resolve
title: "Merchant Resolve"
description: "Canonicalise raw merchant strings from receipts to stable merchant slugs. Suggest new merchants for user approval — never auto-merge."
category: finance
status: active
model_tier: fast
priority: 6
triggers:
  - "resolve merchant"
  - "canonicalise merchant"
  - "is this the same merchant"
tools_granted:
  - resolve_merchant
  - list_merchants
language:
  output_policy: source
---

## What this skill is

Engine substrate for the receipt-processing workflow. After `receipt-ingest` extracts the raw merchant string ("AMZN MKTP US*8XK4N", "Apple EU S.à r.l.", "アマゾン"), this skill resolves it to a canonical merchant slug — or proposes a new merchant for user approval.

The deterministic part lives in `MerchantResolver` (`server/src/agntspace/finance/merchants.py`): exact alias index → POS-noise stripping → null. The skill's job is to handle the **null case**: when no canonical merchant matches, decide whether to propose a new one or merge into an existing similar one.

## When to use

Called by the `bookkeeper` agent during the workflow's `extract` step, immediately after `receipt-ingest` returns a draft with `merchant_raw` set but `merchant_slug` empty.

## Resolution flow

1. Call `resolve_merchant(merchant_raw)`. If returns a slug, done — set the draft's `merchant_slug` and `add_alias` if the raw string differs from any known alias.
2. If null, check `list_merchants(limit=20)` for **visually similar** merchants (same first word, same domain, same country). If you find ONE that's clearly the same entity in a different format, propose `merge_proposal` with both forms and STOP — wait for user approval.
3. If still no match, propose a new merchant with `merchant_raw` as the canonical name (the user can edit at approval time).
4. NEVER auto-merge above the comma-prefix or string-similarity threshold. Session-107 entity-dedup memory is binding: suggest, never auto-classify the middle band.

## Hard rules

- NEVER invent a country code, default category, or canonical name not derivable from the raw string + tier-1/2 lookup.
- NEVER call `upsert_merchant` directly — that's a write-path action that requires user approval. Surface a proposal with the raw string and let the auditor + user decide.
- For non-Latin script merchant names ("北京餐廳"), the resolver's hash-based slug is correct — don't try to transliterate. The display name preserves the original script.

## Examples

- input: |
    Resolve merchant raw: "AMZN MKTP US*8XK4N"
  output: |
    [Calls tool: resolve_merchant with {"raw_text": "AMZN MKTP US*8XK4N"}]
    Resolved to "amazon" (canonical: Amazon). The "US*8XK4N" suffix was auth-code noise; the underlying merchant matches an existing alias.

- input: |
    Resolve merchant raw: "Blue Bottle SF Mission"
  output: |
    [Calls tool: resolve_merchant with {"raw_text": "Blue Bottle SF Mission"}]
    [Calls tool: list_merchants with {"limit": 20}]
    No canonical match. The closest existing merchant is "blue-bottle-coffee" (Blue Bottle Coffee). Proposing merge — please confirm at /finance/merchants.

- input: |
    Resolve merchant raw: "アマゾン"
  output: |
    [Calls tool: resolve_merchant with {"raw_text": "アマゾン"}]
    Resolved to "amazon" via cross-language alias. The Japanese form maps to the canonical Amazon record.
