---
name: libramen
description: Use when the user wants to find, scope, price, compare, check availability for, or book a real-world service provider through Libramen MCP, including local experiences, appointments, trade work, professional services, and other qualified service-provider transactions. Do not use for digital goods, online shopping, or general information queries that do not involve a real-world service provider.
---

# Libramen — Agentic Commerce Skill

> Connect your user to qualified service providers through the Libramen network via MCP. This skill handles discovery, scoping, pricing, and payment for real-world services — from trade projects to professional engagements.

## When to Activate

Activate when the user wants to:
- Scope a service (e.g., "I need my roof re-done", "get a cybersecurity audit quote")
- Engage a professional or trade provider (e.g., "find a consulting firm", "get a plumber")
- Compare qualified service providers for a specific need
- Check availability or pricing for a real-world service

Do NOT activate for:
- Online shopping / e-commerce (digital goods)
- General information queries
- Tasks that don't involve a real-world service provider

---

## Transaction Harness

Build a **Canonical Transaction Summary**, verify it against the user's request, verify amount units, then pay. A mandate token proves Libramen approved a payable amount; it does not by itself prove the agent preserved the user's intended service, date, or headcount.

### Golden Path

1. Write a Canonical Transaction Summary: provider, service, date/time or slot, participant/headcount, selected options, total, currency, payment rail, and mandate expiry.
2. If the user already gave facts, pass them immediately to `evaluate_transaction`. Use `{}` only when the user has not given concrete booking facts and you are intentionally exploring requirements.
3. Accumulate every answer from `nextQuestion`; never replace the parameter bag with only the newest answer.
4. On APPROVED, verify product/package, requested date/slot, participant count, pricing total, currency, payment capability, `paymentAmounts`, and expiry against the summary.
5. Show the final summary to the user and get explicit approval before `confirm_mandate`.

### Safety Invariants

- **Money units:** Libramen `basePrice`, pricing `grandTotal`, and `mandate.amount` are major units. Stripe Link and Stripe API amounts are minor units. For USD/AUD/NZD/CAD/EUR/GBP, `$330.00` is `mandate.amount: 330` and `paymentAmounts.stripeMinorAmount: 33000`. Use `paymentAmounts.stripeMinorAmount` for Stripe Link `--amount`, Link totals, and Stripe minor-unit line items. Do not pass `mandate.amount` directly to Stripe Link unless it is a zero-decimal currency and equals `paymentAmounts.stripeMinorAmount`.
- **Headcount:** for participants, use `parameters.participantCount` unless `nextQuestion.parameterId` asks for a different parameter. Do not use `quantity` as a synonym unless the product explicitly asks for `quantity`.
- **Per-person pricing:** for per-person services, require the approved total to match `unit price * participantCount` plus visible modifiers, or a line item that proves the same. If you cannot prove that 3 people are priced as 3 people, do not pay.
- **Dates:** pass the user's requested booking date as top-level `requestedDate` on `evaluate_transaction`; this triggers the external availability resolver. Do not invent date-ish parameter names unless `nextQuestion.parameterId` asks for one.
- **Mandate expiry:** APPROVED mandates hold slots for 5 minutes. For human approval loops such as Stripe Link, treat the first APPROVED response as a quote, get Link approval where possible, then refresh/re-issue the Libramen mandate and confirm immediately if the old hold is close to expiry.
- **Bad unpaid mandate recovery:** do not confirm a mismatched mandate. Open a fresh SSE session and re-run `evaluate_transaction` with corrected parameters; if competing for the same scarce slot, wait for the old 5-minute hold to release.

---

## Quick Reference

### Tools

| Tool | Purpose | Key Input | Key Output |
|------|---------|-----------|------------|
| `search_businesses` | Discover providers across the network | `search`, `location` | `businesses[]` with `gateway` URL |
| `list_products` | Browse a business's service catalog | `organization` (slug) | `items[]` with name, basePrice |
| `check_availability` | Query available time slots | `productId`, `startDate` | `slots[]` with capacity status |
| `evaluate_transaction` | Qualify a transaction iteratively | `productId`, `parameters` | `status`, `nextQuestion` or `mandate` |
| `confirm_mandate` | Execute payment after user approval | `mandateToken`, `paymentMethod`, `buyer` | `status` (CAPTURED / PAYMENT_REQUIRED / PENDING / PENDING_APPROVAL / FAILED) |
| `get_mandate_status` | Look up mandate state when a buyer asks "how's my booking?" — typically after `PENDING_APPROVAL` (manual-confirm orgs) | `organizationId`, `mandateId` | `status`, `ttlExpiresAt`, `rejectionReason?` |

### Rate Limits

| Metric | Limit |
|--------|-------|
| `search_businesses` per IP | 10 / minute |
| All MCP requests per IP | 40 / minute |
| `evaluate_transaction` per IP+org | 60 / hour |
| `confirm_mandate` per IP+org | 10 / hour |
| `confirm_mandate` attempts per mandate | 3 (then re-issue mandate via `evaluate_transaction`) |
| Distinct orgs per IP | 5 / hour |
| Active mandates per session | 1 |
| Connections per IP | 10 |

---

## Connection

Libramen is a **remote MCP server over SSE**. There is no npm package or local install — register the endpoint as a remote SSE MCP server in your host's MCP config (Claude Code, Cursor, ChatGPT desktop, your own runtime, etc. — exact registration syntax is host-specific; consult your host's "remote MCP server" docs).

- **Endpoint:** `https://api.libramen.ai/api/v1/mcp/sse`
- **Transport:** Server-Sent Events. The SSE stream's first `endpoint` event returns the per-session `/api/v1/mcp/message?sessionId=...` URL — send JSON-RPC 2.0 tool calls there.
- **Auth:** anonymous. No API key, no Bearer token, no signup. The full transaction flow — `search_businesses`, `list_products`, `check_availability`, `evaluate_transaction`, `confirm_mandate`, `get_mandate_status` — is callable without credentials against any org that has enabled discovery.

**Session binding:** Your first per-org tool call locks the session to that business. Calling tools for a different business returns `FORBIDDEN` and requires a new connection. Use one SSE per business.

**Active mandate cap:** A session may have at most one open mandate at a time. Starting a second `evaluate_transaction` flow in the same session before the first is captured, failed, or expired returns `FORBIDDEN`. There is no public cancel tool; for a bad unpaid mandate, open a fresh SSE session and, for slot-sensitive bookings, wait for the old 5-minute hold to release.

**Organization identification:** All tools accept either:
- `organizationId`: UUID
- `organization`: Slug (e.g., `"summit-roofing"`, pattern: `^[a-z0-9-]+$`)

Use slugs when available — they come from search results and are simpler than UUIDs.

---

## Core Flow

### Step 1: Discover Providers

Call `search_businesses` (the query parameter is `search`; `q` is also accepted as an alias):

```json
{
  "search": "roof replacement",
  "location": "Melbourne, Australia",
  "category": "Construction",
  "limit": 10
}
```

**Response:**
```json
{
  "businesses": [
    {
      "organization": {
        "id": "org-uuid",
        "name": "Summit Roofing Co",
        "slug": "summit-roofing",
        "category": "Construction",
        "description": "Residential and commercial roofing — inspections, re-roofing, and repairs",
        "location": { "city": "Melbourne", "country": "Australia" },
        "serviceArea": "LOCAL"
      },
      "matchQuality": "strong",
      "credibility": "established",
      "matchedProducts": [
        { "id": "prod-uuid", "name": "Full Re-Roofing", "category": "roofing" }
      ],
      "gateway": "https://platform.example.com/.well-known/agent-gateway/summit-roofing.json"
    }
  ],
  "totalResults": 1
}
```

**Always include `location`** to avoid cross-geography matches. Prioritize `strong` matches from `established` providers. Use `organization.slug` from the result for subsequent tool calls.

If no results, the response includes a `suggestion` field. Tell the user honestly. Do NOT fall back to web search or unqualified channels.

### Step 2: Inspect the Business (optional)

The `gateway` field on each search result is the org's discovery document — fetch it if you want to see the org's MCP capabilities, accepted payment rails, or any skill overrides. You do **not** need to reconnect: subsequent tool calls use the same Libramen MCP session and just pass the `organization` slug (or `organizationId`) from the search result. Session binding (above) locks your connection to the first org you query.

### Step 3: Browse the Service Catalog

Call `list_products`:

```json
{
  "organization": "summit-roofing",
  "limit": 20,
  "include_packages": true
}
```

**Response:**
```json
{
  "organizationId": "org-uuid",
  "items": [
    { "id": "prod-uuid", "type": "PRODUCT", "name": "Full Re-Roofing", "description": "Complete roof replacement including old roof removal", "category": "roofing", "basePrice": 8500, "currency": "AUD" },
    { "id": "prod-uuid-2", "type": "PRODUCT", "name": "Roof Inspection", "description": "Detailed inspection with written report", "category": "inspections", "basePrice": 350, "currency": "AUD" },
    { "id": "pkg-uuid", "type": "PACKAGE", "name": "Inspection + Re-Roof Bundle", "description": "Inspection, quote, and full re-roofing", "category": "bundles", "basePrice": 8200, "currency": "AUD" }
  ],
  "pagination": { "limit": 20, "offset": 0, "hasMore": false }
}
```

**Products** are individual services. **Packages** bundle multiple products with option groups. Only ACTIVE services are shown.

### Step 4: Check Availability (Optional)

Call `check_availability` if the service is time-bound:

```json
{
  "organization": "summit-roofing",
  "productId": "prod-uuid",
  "startDate": "2026-05-01",
  "endDate": "2026-05-14",
  "participantCount": 1
}
```

**Response:**
```json
{
  "productId": "prod-uuid",
  "dateRange": { "from": "2026-05-01T00:00:00Z", "to": "2026-05-14T00:00:00Z" },
  "slots": [
    { "startTime": "2026-05-05T08:00:00Z", "endTime": "2026-05-05T16:00:00Z", "remainingCapacity": 2, "status": "AVAILABLE" },
    { "startTime": "2026-05-08T08:00:00Z", "endTime": "2026-05-08T16:00:00Z", "remainingCapacity": 1, "status": "LIMITED" },
    { "startTime": "2026-05-12T08:00:00Z", "endTime": "2026-05-12T16:00:00Z", "remainingCapacity": 0, "status": "FULL" }
  ],
  "nextAvailable": { "startTime": "2026-05-05T08:00:00Z", "endTime": "2026-05-05T16:00:00Z", "remainingCapacity": 2 },
  "totalSlots": 3
}
```

Defaults: `startDate` = now, `endDate` = startDate + 7 days. `nextAvailable` searches 30 days ahead.

Not all services are time-slotted. Project-based services (construction, consulting) may not have time slots — availability is most relevant for appointment-based services (inspections, consultations, training). If a product has no availability data, skip this step.

### Step 5: Scope and Qualify

`evaluate_transaction` is the core tool. It uses **progressive disclosure**: pass known facts first, then let the system guide only the missing pieces.

```json
{
  "organization": "summit-roofing",
  "productId": "prod-uuid",
  "parameters": { "roof-area-id": 180 }
}
```

**Optional field:**
- `requestedDate` — ISO date string. When set, the unified availability resolver fires inline against the operator's external live-availability source (calendar, iCal feed, or custom API) in the same call. Pass it instead of (not in addition to) calling `check_availability` separately when you already know the date — saves one tool call and ensures the resolver's slot decision is what binds the mandate.
- `parameters.participantCount` — use this for user headcount on per-person services unless the provider asks for a different parameter ID. `quantity` is not a generic synonym.

If the user request is concrete, include those facts immediately:

```json
{
  "organization": "demo-provider",
  "productId": "bookable-service-id",
  "requestedDate": "2026-05-29",
  "parameters": { "participantCount": 3 }
}
```

#### Response: REQUIRES_MORE_INFO

The system tells you exactly what information to collect from the user:

```json
{
  "organizationId": "org-uuid",
  "status": "REQUIRES_MORE_INFO",
  "isEligible": false,
  "outcomes": [],
  "nextQuestion": {
    "parameterId": "roof-area-id",
    "parameterName": "Roof Area (sqm)",
    "prompt": "What is the approximate roof area in square metres?",
    "type": "NUMBER",
    "helpText": "Measure the footprint of the building — typical house is 150-250 sqm"
  }
}
```

Ask the user for the value. If `nextQuestion.options` is present, present them as choices — don't ask for free text. Then re-call with the answer **added to all previous parameters**:

```json
{
  "organization": "summit-roofing",
  "productId": "prod-uuid",
  "parameters": { "roof-area-id": 180 }
}
```

The system may ask several questions (roof pitch, material preference, access constraints, etc.). Keep iterating — each call returns the next requirement until resolved.

`nextQuestion.type` values you may see: `NUMBER`, `TEXT`, `STRING`, `DATE`, `DATETIME`, `TIME`, `BOOLEAN`, `SELECT`, `MULTISELECT`, `RANGE`. Treat unknown values as free text.

#### Response: BLOCKED

A business requirement wasn't met. The `blockReason.message` is human-readable — surface it to the user, ask them to adjust their input, then re-run `evaluate_transaction`.

```json
{
  "organizationId": "org-uuid",
  "status": "BLOCKED",
  "isEligible": false,
  "outcomes": [],
  "blockReason": {
    "constraintId": "c-uuid",
    "constraintName": "Minimum Roof Area",
    "message": "Value must be at least 50"
  }
}
```

If a requirement can't be met:
1. Re-run `evaluate_transaction` with adjusted parameters after the user clarifies.
2. Browse other services from the same provider (`list_products`) — a different product may have different requirements.
3. Search for alternative providers (`search_businesses`) with adjusted criteria.
4. Tell the user honestly that this provider can't accommodate their needs.

#### Response: APPROVED

The transaction is fully qualified with pricing:

```json
{
  "organizationId": "org-uuid",
  "status": "APPROVED",
  "isEligible": true,
  "outcomes": [
    { "type": "PRICING", "value": { "grandTotal": 9200, "currency": "AUD" } }
  ],
  "mandate": {
    "token": "eyJhbGciOi...",
    "amount": 9200,
    "currency": "AUD",
    "expiresAt": "2026-04-17T14:05:00Z"
  },
  "paymentAmounts": {
    "libramenMajorAmount": 9200,
    "stripeMinorAmount": 920000,
    "currency": "AUD",
    "stripeCurrency": "aud"
  },
  "available_payment_methods": ["X402", "STRIPE"],
  "paymentMethods": {
    "x402": true,
    "stripeSpt": true,
    "stripe": { "networkId": "profile_..." },
    "mpp": {
      "methods": ["tempo", "stripe"],
      "httpPayUrl": "https://api.libramen.ai/api/v1/mpp/pay/<orgId>/mandate/<mandateId>/confirm",
      "tempoRecipientAddress": "0x...",
      "testnet": false,
      "stripe": { "networkId": "profile_...", "paymentMethodTypes": ["card", "link"] }
    },
    "googlePay": null
  }
}
```

Before payment, build and show the Canonical Transaction Summary. Verify:

- `mandate.amount`, `paymentAmounts.libramenMajorAmount`, and the PRICING `grandTotal` match; these are Libramen major-unit amounts.
- `mandate.currency` equals the PRICING currency.
- For Stripe Link, `paymentAmounts.stripeMinorAmount` is the minor-unit amount the buyer must approve.
- Your submitted `requestedDate`, `participantCount`, and selected options match the user's request.
- Any per-person total is explainable, e.g. `3 x $110.00 = $330.00`, `mandate.amount = 330`, and `paymentAmounts.stripeMinorAmount = 33000`.
- The current time leaves enough runway before `mandate.expiresAt` to finish payment.

**Slot reservation:** APPROVED immediately reserves the first available slot. The reservation is held for 5 minutes (the mandate expiry). If the mandate expires without payment, the reservation releases automatically.

**Choosing a payment method:** read the structured `paymentMethods` field for unambiguous capability discovery. Use `paymentMethods.mpp.methods` to distinguish Tempo MPP (`"tempo"`) from Stripe/Link MPP (`"stripe"`). For Stripe Link, prefer `paymentMethods.mpp.httpPayUrl` with `link-cli mpp pay` when `"stripe"` is listed. Use direct `STRIPE_SPT` only when a wallet gives you a raw `shared_payment_token.id` / `spt_xxx`; `paymentMethods.stripe.networkId` is the merchant Stripe profile ID for raw-SPT wallets. The `available_payment_methods` array is a coarse hint with provider names (`X402`, `STRIPE`, `MPP`); do NOT rely on it to detect SPT capability — `STRIPE_SPT` does not appear there.

#### Optional: Packages with Options

For packages with option groups, pass `selectedOptions`:

```json
{
  "organization": "summit-roofing",
  "packageId": "pkg-uuid",
  "parameters": { "roof-area-id": 180 },
  "selectedOptions": { "material-group-id": "colorbond-option-id" }
}
```

### Step 5b: Get Mandate Status (manual-approval flow)

Call `get_mandate_status({ organizationId, mandateId })` when the buyer asks for an update on a `PENDING_APPROVAL` mandate. **This is a lookup, not a polling endpoint.** Rate limits apply per mandate.

Returns one of:

| `status` | Meaning |
|----------|---------|
| `PENDING_APPROVAL` | Operator hasn't decided yet. `ttlExpiresAt` populated — communicate that window to the buyer. |
| `CONFIRMED` | Operator approved (manual-confirm) OR an X402 HTTP settlement is in progress. Capture / settlement still pending. |
| `EXECUTED` | Settled. Payment captured; booking is committed. |
| `REJECTED` | Operator declined. `rejectionReason` may be populated — relay verbatim to the buyer. |
| `EXPIRED` | Operator didn't respond in time. Slot released, no payment. |
| `PAYMENT_FAILED` | Operator approved but the card capture failed. Slot released — buyer can try again with a different payment method. |
| `CANCELLED` | Mandate was cancelled before settlement (buyer-initiated or operator-initiated). |
| `PENDING` | Payment in progress (e.g., Stripe async-capture didn't resolve within the long-poll window). |
| `NOT_FOUND` | Wrong org for this mandate, or unknown mandate. |

### Step 6: Confirm Payment

**Never call `confirm_mandate` without explicit user approval of the amount.**

`confirm_mandate` requires three things: the mandate token, an explicit `paymentMethod`, and a `buyer` object. **Omitting `paymentMethod` returns `FAILED { errorCode: "MISSING_PAYMENT_METHOD" }`** — there is no implicit default. The error message lists the rails the merchant has configured; pick one explicitly.

**Confirmation channel.** For inline outcomes (`CAPTURED` / `PAYMENT_REQUIRED` / `PENDING` / `FAILED`) the agent is the buyer's confirmation channel — relay the result directly. The buyer also always receives an email receipt for every successful booking, so you do not need to be the only channel.

**`PENDING_APPROVAL` status — operator manual approval flow (the exception).** Some operators require human review before any booking commits. When `confirm_mandate` returns `{ status: "PENDING_APPROVAL", mandateId, ttlExpiresAt }`, do NOT poll. Tell the buyer the operator will respond within the time window indicated by `ttlExpiresAt`, then disconnect — operator review is asynchronous and may take hours. From this point the buyer's email is the canonical channel: the operator's lifecycle emails (pending → approved / rejected / expired / payment-failed) drive status, not the agent. If the buyer asks the agent for an update before an email arrives (or on a fresh reconnect), call `get_mandate_status({ organizationId, mandateId })` (Step 5b). Operators on manual-confirm orgs only support **STRIPE_SPT** and **CARD_TOKEN** rails; presenting `X402` or `MPP` returns `FAILED { errorCode: "MANUAL_CONFIRM_RAIL_UNSUPPORTED" }`.

**Mandate state after confirm.** For X402, `confirm_mandate` returns `PAYMENT_REQUIRED` with x402 v2 HTTP settlement details. Pay the returned `x402.settleUrl` with the returned `x402.body`; the HTTP settlement endpoint verifies payment, commits the booking, and advances the mandate to `EXECUTED`. For instant rails (Stripe SPT, CARD_TOKEN, MPP), a `CAPTURED` response means the mandate is already `EXECUTED`.

#### Buyer Identity

`buyer` is **required** on every `confirm_mandate` call. Missing or malformed buyer returns `FAILED { errorCode: "VALIDATION_ERROR" }` and the payment is never attempted. The business needs this to deliver the service, send receipts, and defend chargebacks.

Required fields:
- `email` — RFC 5322 valid address.
- `phone_number` — E.164 format recommended (e.g., `"+14155551234"`).
- A name: either `full_name` OR both `first_name` and `last_name`.

Optional fields:
- `customer_id` — your stable identifier for this buyer (pass-through, not stored by Libramen).
- `account_type` — `"guest"`, `"registered"`, or `"business"`.

Collect this information **after the APPROVED response, before calling `confirm_mandate`** — never during the scoping/qualification flow.

#### Request

Full-name variant:

```json
{
  "organization": "summit-roofing",
  "mandateToken": "eyJhbGciOi...",
  "paymentMethod": { "type": "X402" },
  "buyer": {
    "email": "alex@example.com",
    "phone_number": "+14155551234",
    "full_name": "Alex Example"
  }
}
```

Split-name variant:

```json
{
  "organization": "summit-roofing",
  "mandateToken": "eyJhbGciOi...",
  "paymentMethod": { "type": "STRIPE_SPT", "sptToken": "spt_test_xxx" },
  "buyer": {
    "email": "alex@example.com",
    "phone_number": "+14155551234",
    "first_name": "Alex",
    "last_name": "Example",
    "account_type": "registered"
  }
}
```

#### Payment Methods

| Type | Description | Required Field | Provenance |
|------|-------------|----------------|------------|
| `X402` | Stablecoin/USDC via x402 v2 HTTP settlement | none | The x402 client signs the HTTP `PAYMENT-REQUIRED` challenge; do not broadcast a manual transfer. |
| `STRIPE_SPT` | Stripe Shared Payment Token (SPT) | `sptToken` (e.g. `spt_xxx`) | Minted by a wallet that can issue Stripe SPTs. Link can issue SPTs when supported by the Link account, merchant profile, and approved spend request; do not assume it will always expose a token. The buyer approves a single-use credential on their device, then you pass `sptToken` to `confirm_mandate`. Libramen does not issue these. |
| `CARD_TOKEN` | Stripe `tok_xxx` / `pm_xxx` payment-method token | `token` | You must integrate Stripe Elements or Payment Intents on your side to obtain the token. |
| `MPP` | Machine Payments Protocol credential | Standard `_meta["org.paymentauth/credential"]` object on the MCP request; legacy `mppCredential` / `"Payment ..."` strings remain accepted. HTTP MPP uses `paymentMethods.mpp.httpPayUrl` instead | Issued by your MPP provider; Libramen does not issue these. For MCP v1, `MPP` is Tempo-oriented and mandate challenges are pull-only (`supportedModes: ["pull"]`). Tempo MPP requires a compatible stablecoin wallet and enough TIP-20 stablecoin to cover payment plus Tempo fees. Stripe/Link MPP is HTTP-only: use `link-cli mpp pay` against `paymentMethods.mpp.httpPayUrl` when `paymentMethods.mpp.methods` includes `"stripe"`. |

#### Stripe Link MPP and raw SPT via Link CLI

Use the HTTP MPP path first when `paymentMethods.mpp.methods` includes `"stripe"` and `paymentMethods.mpp.httpPayUrl` is present. Use the direct raw-SPT fallback only when `paymentMethods.stripeSpt === true` and Link or another wallet returns a concrete raw `spt_xxx` in `shared_payment_token.id`.

Protective checklist:

1. Verify `paymentMethods.stripe.networkId` is present; this is the merchant Stripe profile ID.
2. Verify `mandate.amount === PRICING.grandTotal === paymentAmounts.libramenMajorAmount`; these are Libramen major-unit amounts.
3. Verify the Canonical Transaction Summary matches the user's request: service, date/slot, participant count, total, and currency.
4. Use `paymentAmounts.stripeMinorAmount` exactly for Link `--amount`, Link total amount, and any Stripe minor-unit line items. Never use `mandate.amount` as the Link amount for two-decimal currencies.
5. Put the same summary in Link `--context`; include participant count and date.
6. Abort on any mismatch: wrong merchant network, wrong Link amount, wrong currency, wrong service/date/participants, expired mandate, or missing `shared_payment_token.id` in the retrieve response. If a `$330.00` booking shows as `$3.30` in Link, deny/abort; the Link amount is wrong.
7. If the 5-minute Libramen hold is close to expiry after Link approval, refresh/re-issue the Libramen mandate in a fresh SSE session if needed and confirm immediately only if amount, currency, merchant, service, date, and participants still match.

Preferred Stripe Link MPP path:

```sh
link-cli mpp pay <paymentMethods.mpp.httpPayUrl> \
  --merchant-name "<business name>" \
  --merchant-url "<business website or Libramen business URL>" \
  --network-id <paymentMethods.mpp.stripe.networkId> \
  --amount <paymentAmounts.stripeMinorAmount> \
  --currency <paymentAmounts.stripeCurrency> \
  --context "<100+ char human-readable booking description>"
```

Send the same `mandateToken` and `buyer` body the Libramen HTTP MPP endpoint expects when your Link/MPP client prompts for request body fields. The HTTP MPP endpoint performs settlement and mandate execution; do not then call MCP `confirm_mandate` for the same mandate.

Direct raw-SPT fallback:

Authenticate Link and choose a saved payment method:

```sh
link-cli auth status
link-cli auth login --client-name "Your agent name" # only if not authenticated
link-cli payment-methods list --format json
```

Create the spend request. For direct SPT requests, pass `--credential-type shared_payment_token`, `--network-id`, `--merchant-name`, and `--merchant-url`. `--context` must be at least 100 characters and must describe the exact Libramen booking the buyer is approving.

```sh
link-cli spend-request create \
  --credential-type shared_payment_token \
  --network-id <paymentMethods.stripe.networkId> \
  --merchant-name "<business name>" \
  --merchant-url "<business website or Libramen business URL>" \
  --payment-method-id <link_payment_method_id> \
  --amount <paymentAmounts.stripeMinorAmount> \
  --currency <paymentAmounts.stripeCurrency> \
  --context "<100+ char human-readable booking description>" \
  --line-item "name:<service name>,unit_amount:<minor-unit unit price or paymentAmounts.stripeMinorAmount>,quantity:<participant count or 1>" \
  --total "type:total,display_text:Total,amount:<paymentAmounts.stripeMinorAmount>" \
  --request-approval \
  --format json
```

For per-person bookings, prefer a real minor-unit line item such as unit amount `11000`, quantity `3`, total `33000`. If you only have a safe total and no trustworthy unit amount, use quantity `1`, set the total to `paymentAmounts.stripeMinorAmount`, and put the participant breakdown in `--context`; do not invent a unit price.

`--request-approval` pushes the approval to Link. In agent/non-TTY mode, follow the returned `_next.command`; otherwise resume polling manually:

```sh
link-cli spend-request retrieve <lsrq_id> \
  --interval 2 \
  --max-attempts 300 \
  --format json
```

After the spend request status is approved, retrieve the actual single-use SPT. This include is required; a plain retrieve may omit the credential.

```sh
link-cli spend-request retrieve <lsrq_id> \
  --include shared_payment_token \
  --format json
```

Read `shared_payment_token.id` from the JSON response and pass it to Libramen. Abort if the retrieve response is missing `shared_payment_token.id` or the value does not start with `spt_`.

```json
{
  "paymentMethod": {
    "type": "STRIPE_SPT",
    "sptToken": "spt_xxx"
  }
}
```

SPTs are single-use and short-lived. If `confirm_mandate` returns `CREDENTIAL_EXPIRED`, `WRONG_AMOUNT`, `WRONG_MERCHANT`, `CARD_DECLINED`, or another typed SPT error, create a fresh spend request only when the recovery table says retry is appropriate. Stripe SPT confirmation may wait up to 90 seconds for webhook settlement; MCP clients should use `resetTimeoutOnProgress: true` or a request timeout of at least 100 seconds.

**MPP probe transport (important — different from X402).** Calling `confirm_mandate` with `paymentMethod: { type: "MPP" }` and **no credential** is a probe. The server responds with a **JSON-RPC error code `-32042`** carrying standard Payment Auth challenges in `error.data.challenges[]` plus `error.data.httpStatus === 402` and `error.data.problem`. Legacy `error.data.mppChallenge` is also present during the compatibility window. Catch the JSON-RPC error, present `error.data.challenges[0]` to your MPP provider, then retry `confirm_mandate` with the resulting standard credential object in request `_meta["org.paymentauth/credential"]`. Legacy `"Payment ..."` strings in `_meta` or `paymentMethod.mppCredential` are still accepted. Don't write `if (result.status === 'PAYMENT_REQUIRED')` for MPP — that path is X402-only.

Tempo mandate MPP challenges are pull-only: the challenge request includes `methodDetails.supportedModes: ["pull"]`. Return a signed Tempo transaction credential for that exact challenge. Do not use push-mode, hash/proof receipts, or a transaction for a different challenge, mandate, amount, recipient, currency, or realm. Libramen verifies the signed transaction, reserves replay keys, commits the mandate, then broadcasts; rejected credentials return JSON-RPC `-32043` with a fresh challenge when one can be issued.

On successful paid MCP MPP confirmation, read the standard receipt from result `_meta["org.paymentauth/receipt"]`. The receipt includes `status`, `method`, `timestamp`, `reference`, and `challengeId`. Stripe/Link MPP does not use this MCP flow; it remains HTTP-only through `paymentMethods.mpp.httpPayUrl`.

#### Responses

**CAPTURED** — payment succeeded:
```json
{ "status": "CAPTURED", "paymentId": "pi_..." }
```

`paymentId` shape depends on rail: Stripe SPT / CARD_TOKEN return the Stripe PaymentIntent id (`pi_...`). Stripe MPP also returns the Stripe PaymentIntent id (`pi_...`) from the HTTP MPP receipt. Tempo MPP returns the Tempo tx / receipt reference (often a 0x-prefixed transaction hash) for the Tempo explorer. X402 returns the settlement transaction reference from the x402 facilitator.

**PAYMENT_REQUIRED** — X402 v2 HTTP settlement required. POST the returned `x402.body` to `x402.settleUrl` using an x402 v2-capable HTTP client. The client receives a `PAYMENT-REQUIRED` header, signs/pays the exact USDC requirement, retries the same POST with `PAYMENT-SIGNATURE`, and the server settles the mandate. The endpoint advertises the optional x402 `payment-identifier` extension for idempotency; clients may include it, but extension-less clients are valid.
```json
{
  "status": "PAYMENT_REQUIRED",
  "x402": {
    "version": 2,
    "method": "POST",
    "settleUrl": "https://api.libramen.ai/api/v1/x402/settle",
    "body": { "x402PaymentToken": "x402sess_..." },
    "scheme": "exact",
    "network": "eip155:84532",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "amountAtomic": "12340000",
    "expiresAt": "2026-04-17T14:20:00Z"
  }
}
```

Supported X402 networks are Base mainnet (`eip155:8453`) and Base Sepolia (`eip155:84532`) with exact USDC. Ethereum mainnet is not supported.

**PENDING** — payment in progress (e.g., Stripe async-capture didn't resolve within the 90s server-side wait):
```json
{ "status": "PENDING", "paymentId": "pi_..." }
```

**FAILED** — payment failed:
```json
{ "status": "FAILED", "error": "Insufficient funds", "errorCode": "PAYMENT_FAILED" }
```

#### Cancellation Policy

When the operator has configured a support contact, every non-FAILED `confirm_mandate` response (CAPTURED, PAYMENT_REQUIRED, PENDING, PENDING_APPROVAL) carries:

```json
{
  "cancellationPolicy": {
    "method": "contact_operator",
    "contactEmail": "support@example.com",
    "instructions": "..."
  }
}
```

Relay `contactEmail` and `instructions` to the buyer so they know where to request a cancellation or refund. Libramen does not process refunds — the operator handles money movement on their side.

#### Stripe SPT error codes

When the rail is `STRIPE_SPT`, Libramen returns one of these typed `errorCode` values so you can pick the right recovery action without parsing free-form text:

| `errorCode` | What it means | Recovery |
|-------------|---------------|----------|
| `WRONG_AMOUNT` | The SPT was authorised for a different Stripe minor-unit amount than this booking requires. | Re-mint an SPT for `paymentAmounts.stripeMinorAmount`. |
| `CREDENTIAL_EXPIRED` | The SPT's TTL elapsed. | Re-mint a fresh SPT and retry. |
| `WRONG_MERCHANT` | The SPT is bound to a different merchant's network_id. | Terminal — re-minting won't help. Do not retry against this merchant. The error message is generic by design (won't echo any other merchant's identity). |
| `CARD_DECLINED` | Underlying card was declined. | Surface to the user; they may need a different funding source. |
| `MANDATE_STATE_MISMATCH` | The mandate has already advanced past `PENDING` (e.g. concurrent confirm). | Don't retry — re-evaluate. |
| `PAYMENT_INTENT_FAILED` | Stripe rejected the intent (catch-all). | Retry once with a fresh SPT; if it persists, surface to the user. |
| `PAYMENT_FAILED` | Catch-all for unmapped Stripe failures. | Retry with a fresh SPT. |
| `CAPTURE_FAILED` | Auth succeeded but capture failed. | Retry once. |

#### Paying with Stripe Link

Stripe Link agents, including agents using [`link-cli`](https://github.com/stripe/link-cli), should use the detailed **Stripe Link MPP and raw SPT via Link CLI** flow above. Preferred handoff: pay `paymentMethods.mpp.httpPayUrl` with `link-cli mpp pay` when `paymentMethods.mpp.methods` includes `"stripe"`. Direct raw-SPT fallback: create a spend request for `paymentMethods.stripe.networkId`, wait for approval, retrieve it with `--include shared_payment_token`, then pass `shared_payment_token.id` as `paymentMethod.sptToken`.

**Long-poll caveat.** For 3DS or async-capture flows, Libramen holds the response open up to 90 seconds while it waits for Stripe to confirm settlement. Set your MCP client's `requestTimeout` to ≥ 100 s, or pass both `resetTimeoutOnProgress: true` AND a `progressToken` on the request — without `progressToken`, no progress events fire and `resetTimeoutOnProgress` has nothing to reset against. Handle the `errorCode` per the table above.

### Step 7: Settle X402 (only when `confirm_mandate` returned PAYMENT_REQUIRED)

After receiving the X402 response, pay the returned HTTP endpoint with an x402 v2 client. Use the returned `x402.settleUrl` as the URL and `x402.body` as the JSON request body. The first POST returns a `PAYMENT-REQUIRED` header; your x402 client signs/pays the exact USDC requirement and retries with `PAYMENT-SIGNATURE`. A successful retry returns `CAPTURED`.

```json
{
  "x402PaymentToken": "x402sess_..."
}
```

**Response — CAPTURED:**
```json
{
  "status": "CAPTURED",
  "mandateId": "00000000-0000-4000-8000-...",
  "bookingId": "00000000-0000-4000-8000-...",
  "paymentId": "0xabc..."
}
```

**Response — FAILED:**
```json
{
  "status": "FAILED",
  "error": "X402 settlement failed",
  "errorCode": "SETTLEMENT_FAILED"
}
```

The server verifies and settles through the x402 facilitator. If settlement fails, start a fresh mandate unless the facilitator error clearly says a client-side retry is safe.

### Step 8: Cleanup

Close SSE connections when the transaction completes or the user abandons.

---

## Error Handling

Most errors arrive as JSON-RPC 2.0 envelopes with an `error.data.errorCode`. Two outliers:
- **Connection-limit errors** (more than 10 concurrent SSE per IP) come back as **plain HTTP 429** with body `{ error, message, limit, current }` — not JSON-RPC.
- **MPP probes** come back as JSON-RPC `-32042` with standard challenges in `error.data.challenges[]` and legacy `error.data.mppChallenge` during the compatibility window (not a `FAILED` tool result).

Example envelope:

```json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32005,
    "message": "Rate limit exceeded",
    "data": { "errorCode": "RATE_LIMIT_EXCEEDED", "retryAfterSeconds": 30 }
  },
  "id": null
}
```

### Error Codes

| `errorCode` | Where | Recovery |
|-------------|-------|----------|
| `VALIDATION_ERROR` | any | Fix request shape (missing fields, wrong types, malformed buyer) |
| `MISSING_PAYMENT_METHOD` | confirm | Pick a rail and re-call |
| `MISSING_TOKEN` | confirm | Provide `token` (CARD_TOKEN) or `sptToken` (STRIPE_SPT) |
| `INVALID_TOKEN` | confirm / finalize | Mandate token is bad or tampered — re-evaluate |
| `EXPIRED` | confirm / finalize | Mandate window passed — re-evaluate |
| `ALREADY_USED` | confirm | Mandate already consumed — re-evaluate |
| `FORBIDDEN` | any | Wrong org for this session OR another mandate is still open. For a bad unpaid mandate, open a fresh SSE session; for the same scarce slot, wait for the old 5-minute hold to release before re-evaluating. |
| `NOT_FOUND` | any | Unknown product, organization, or mandate — check IDs |
| `RATE_LIMIT_EXCEEDED` | any | Honor `retryAfterSeconds` |
| `RAIL_TIMEOUT` | confirm | Rail didn't settle in time — retry once; if it persists, try a different rail |
| `PROVIDER_NOT_CONFIGURED` | confirm | Merchant hasn't finished payment setup for this rail — pick another |
| `MANUAL_CONFIRM_RAIL_UNSUPPORTED` | confirm | Manual-confirm org only accepts `STRIPE_SPT` or `CARD_TOKEN` |
| `EXTERNAL_SOURCE_REJECTED` | confirm | Slot was free at evaluate time but the operator's live calendar reported it busy at confirm. Mandate auto-cancels — re-evaluate with a different date or product. |
| `X402_UNSUPPORTED_REQUIREMENT` | confirm (X402) | Merchant config or mandate currency/amount cannot be expressed as exact USDC on Base/Base Sepolia — pick another rail |
| `SETTLEMENT_FAILED` | X402 HTTP settle | x402 facilitator rejected settlement — retry once with a fresh mandate/payment if the error is transient |
| `MPP_CREDENTIAL_REJECTED` | confirm (MPP) | Credential was malformed, unsupported, replayed, or failed verification. Use a fresh `-32042` challenge and re-collect approval if the user still wants to proceed. |
| `MPP_SETTLEMENT_UNCERTAIN` | confirm (Tempo MPP) | Tempo broadcast became uncertain after the mandate committed. Do not retry or create a second payment. Tell the buyer the operator must reconcile the payment/booking. |
| `CAPTURE_FAILED` | confirm (Stripe) | Authorisation succeeded but capture failed — retry once |
| `SERVICE_UNAVAILABLE` | any | Engine not configured — try another provider |

### JSON-RPC error codes

| Code | Meaning | Action |
|------|---------|--------|
| `-32005` | Rate limit exceeded | Wait `retryAfterSeconds`, then retry |
| `-32042` | Payment required (MPP probe) | Complete `error.data.challenges[0]`, then retry with `_meta["org.paymentauth/credential"]` |
| `-32043` | Payment verification failed (MPP credential rejected/replayed) | Treat the credential as spent or invalid; use the fresh `error.data.challenges[0]` only after user approval still matches |

---

## Discovery Fallback (robots.txt)

If `search_businesses` returns no results and the user has a specific business website:

1. Fetch `{business_url}/robots.txt`
2. Look for: `Agent-Gateway: https://platform.com/.well-known/agent-gateway/{slug}.json`
3. If found, fetch that URL — it returns a discovery document with the MCP endpoint
4. Connect via the `endpoints.mcp_sse` value from the discovery document

Most businesses will NOT have this configured. If absent, tell the user the business isn't set up for agent transactions. Do not fabricate engagements through unqualified channels.

---

## Gotchas

| Mistake | Fix |
|---------|-----|
| Searching without location | Always include `location` — avoids matching a provider in the wrong country |
| Using UUIDs when slugs work | `organization: "summit-roofing"` is simpler than passing UUIDs |
| Sending both `productId` and `packageId` | Exactly one must be provided, never both |
| Starting concrete requests with `{}` | Pass known facts immediately; `{}` is only for exploratory scoping |
| Using `quantity` for headcount | Use `participantCount` unless the provider explicitly asks for `quantity` |
| Passing `mandate.amount` to Stripe Link | Libramen amounts are major units; Stripe Link uses minor units. Use `paymentAmounts.stripeMinorAmount` (`$330.00` -> `33000`). |
| Trusting a mandate without a summary | Build and verify the Canonical Transaction Summary before payment |
| Not accumulating parameters | Each `evaluate_transaction` call must include ALL previous answers + the new one |
| Ignoring `nextQuestion.type` | The `type` field tells you the expected value format |
| Treating BLOCKED as a dead end | Read `blockReason.message` and re-run `evaluate_transaction` with adjusted parameters |
| Calling `confirm_mandate` without `paymentMethod` | It's required — no implicit default. Returns `MISSING_PAYMENT_METHOD` |
| Calling `confirm_mandate` without `buyer` | The buyer object (email + phone + name) is required for the business to deliver the service |
| Reading `available_payment_methods` to detect SPT | Read `paymentMethods.stripeSpt` instead — `STRIPE_SPT` doesn't appear in the array |
| Calling MCP `confirm_mandate` after `link-cli mpp pay` | Don't double-confirm. The HTTP MPP endpoint settles payment and executes the mandate. |
| Paying Tempo MCP MPP with push/hash proof | Use the pull-mode transaction credential from `error.data.challenges[0]`; mandate MPP rejects push/hash credentials. |
| Stopping at PAYMENT_REQUIRED for X402 | POST `x402.body` to `x402.settleUrl` with an x402 v2 HTTP client |
| Treating X402 like a manual tx-hash flow | There is no MCP finalize tool. The x402 HTTP exchange verifies and settles the payment. |
| Mandate expired before confirming | Mandates expire in 5 minutes — confirm promptly after APPROVED |
| Confirming without user consent | Never call `confirm_mandate` without the user explicitly approving the amount |
| Switching orgs mid-session | Session locks to first org queried — switch requires a new connection |
| Starting a second mandate while one is open | Returns `FORBIDDEN` in the same session — use a fresh SSE session for corrected unpaid mandates |
| Burning the 3-attempt mandate cap | Each `confirm_mandate` call counts; after 3 failures the mandate is dead — re-issue via `evaluate_transaction` |
| Ignoring `retryAfterSeconds` | Rate limit errors include retry timing — honor it, don't hammer |
| Skipping availability check | Not all services are time-slotted, but for those that are, call `check_availability` (or pass `requestedDate` to `evaluate_transaction`) before scoping |
| "MCP not installed" / host can't find Libramen | Libramen is a remote SSE MCP server, not a local package. Register `https://api.libramen.ai/api/v1/mcp/sse` as a remote SSE server in your host's MCP config — there's nothing to npm-install |

---

## Example: Consulting Engagement

Here's the same flow for a professional services scenario:

```
1. search_businesses: { "search": "cybersecurity audit", "location": "London, UK" }
   → CyberShield Consulting (slug: "cybershield", credibility: "established")

2. Fetch the gateway URL → connect to endpoints.mcp_sse
   list_products on the cybershield session
   → "Compliance Audit" (£12,000), "Penetration Test" (£8,000), "Full Security Review Package" (£18,000)

3. evaluate_transaction: { "productId": "audit-id", "parameters": {} }
   → REQUIRES_MORE_INFO: "How many employees does your organization have?" (type: NUMBER)

4. evaluate_transaction: { "parameters": { "employee-count": 200 } }
   → REQUIRES_MORE_INFO: "Which compliance framework?" (type: SELECT, options: ["ISO 27001", "SOC 2", "PCI DSS"])

5. evaluate_transaction: { "parameters": { "employee-count": 200, "framework": "SOC 2" } }
   → REQUIRES_MORE_INFO: "What is your target completion date?" (type: DATE)

6. evaluate_transaction: { "parameters": { ..., "target-date": "2026-07-01" } }
   → APPROVED: £14,500 GBP, mandate token issued, paymentMethods.stripeSpt: true

7. Present price to user; collect buyer email/phone/name; user approves.
8. confirm_mandate: { mandateToken, paymentMethod: { type: "STRIPE_SPT", sptToken }, buyer: {...} }
   → CAPTURED + cancellationPolicy with the operator's support email
```

---

## Core Principle

**Knowing you don't know is more valuable than fabricating an engagement.**

This skill connects to businesses that have modeled their real operational requirements into a machine-navigable system. The `evaluate_transaction` pipeline doesn't guess — it evaluates against deterministic rules, real availability, and actual pricing. A "no match" or "BLOCKED" result is honest and useful. A fabricated engagement through an unqualified channel wastes the user's time and erodes trust.
