# Favor Request Actions
URL: /docs/building-apps/capabilities/favor-request-actions
Markdown: /docs/building-apps/capabilities/favor-request-actions.md
Description: Charge favors before Rome dispatches one of your app's actions.

Favor request actions let an app ask the current Rome Cloud visitor to pay favors
before Rome runs an action on the app owner's instance. Use them when a public or
shared app surface offers a paid operation: your app creates a favor action
request, the visitor approves or declines it, and Rome dispatches the configured
local action only after the payment settles.

> **Note:** Favor request actions include a transaction fee. For a limited time,
> that fee is 0%.

> **Experimental:** `favorRequirement` is an experimental action feature. Its
> schema and runtime behavior may change while favor-gated app workflows are
> still being developed.

This is different from `requiresApproval` on an action. `requiresApproval` pauses
the guardian's own action call for permission; a favor request action charges a
visitor account and then queues an owner-side action dispatch.
An action cannot set both `requiresApproval: true` and `favorRequirement`.

## Flow [#flow]

1. Add `favorRequirement` to the top-level action's `action.yaml`.
2. Implement and list that action in `app.yaml`.
3. From an authenticated app API handler, call `ctx.favors.requestAction(...)`.
4. Send the returned `authorizationUrl` to the browser when the result is
   `pending_consent`.
5. After the visitor pays, Rome queues the request and the owner's instance runs
   the configured action.

Request creation must happen from the app API runtime. The browser should call
your app API with `fetchAppApi`; do not post directly to Rome's internal
`/favors/action-requests` endpoint.

## Declare the favor requirement [#declare-the-favor-requirement]

Add `favorRequirement` to the action's `action.yaml`. `amount` is a positive
integer number of favors — a single request is capped at 100,000 favors,
enforced by Rome Cloud when the request is created, not at install; `title` and
`summary` are shown on the payment screen; and `displayFields` selects values
from the validated action args.

The payment recipient is always the hosted app owner. There is no recipient
field.

The app manifest only lists the action artifact:

```yaml
formatVersion: 2
id: report-shop
name: Report Shop
version: 1.0.0
description: Sell report summaries for favors.

api:
  entry: api/index

actions:
  - actions/summarize-report
```

The action carries its own favor requirement:

```yaml
name: summarize_report
type: custom
description: Create a concise summary from a report URL.
entry: ./index.ts
complexity: moderate
speed: slow
reliability: medium
sideEffects: write
favorRequirement:
  amount: 25
  title: Summarize a report
  summary: Create a concise summary from a report URL.
  displayFields:
    - label: Report
      from: $.reportUrl
    - label: Tone
      from: $.tone
```

The action implementation **must** expose an `inputSchema` (`defineAction`
builds one from your Zod schema) — a favor-gated action without one is never
synced to Rome Cloud. Rome derives the payment definition from the registered
action config plus that input schema.

`displayFields[].from` values that start with `$.` are pointers into the
validated args. A template must be the whole value (`$.reportUrl`), not a string
with interpolation inside it.

Only the action named by the favor request is charged. If that action calls
another action, a nested `favorRequirement` on the child action is ignored for
that execution.

The action definition's `name` is local (`summarize_report` above), but favor
requests use its canonical ID: `<app-id>:<local-name>`. Rome Cloud rejects a
request whose namespace does not match the requesting app.

## Create the request from your API [#create-the-request-from-your-api]

Call `ctx.favors.requestAction` from your app API handler. This call uses
the authenticated request's visitor session, including a server-side favor token
that browser code cannot read.

```ts
import type {
  RomeAppApiHandler,
  RomeAppApiRequest,
  RomeAppContext,
} from "@rome-os/app-runtime";

function readJson<T>(request: RomeAppApiRequest): T {
  if (!request.body) throw new Error("request body required");
  return JSON.parse(new TextDecoder().decode(request.body)) as T;
}

export function createApiHandler(ctx: RomeAppContext): RomeAppApiHandler {
  return {
    async handle(request) {
      if (request.method !== "POST" || request.path.join("/") !== "summaries") {
        return Response.json({ error: "not_found" }, { status: 404 });
      }

      const body = readJson<{ reportUrl: string; tone?: string; returnTo?: string }>(request);
      const payerId =
        request.caller.kind === "visitor" ? request.caller.accountId : "anonymous";
      const result = await ctx.favors.requestAction({
        actionName: "report-shop:summarize_report",
        args: {
          reportUrl: body.reportUrl,
          tone: body.tone ?? "neutral",
        },
        taskRef: { kind: "summary", reportUrl: body.reportUrl },
        idempotencyKey: `summary:${body.reportUrl}:${payerId}`,
        // Where the payer lands after paying or declining — typically the page
        // that asked, forwarded from the browser. Must be a path inside
        // /apps/<appId>; when omitted, payers land on /apps/<appId>.
        returnTo: body.returnTo,
      });

      return Response.json(result, {
        status: result.status === "error" ? 400 : 200,
      });
    },
  };
}
```

Use an idempotency key that is stable for the same payer and business operation.
Retries with the same key return the existing request instead of creating a
second charge request. Use a new key for a genuinely new purchase.

## Handle the result in your UI [#handle-the-result-in-your-ui]

From your React app, call your API route and branch on the result status:

```ts
import { fetchAppApi } from "@rome-os/app-web-sdk";

async function buySummary(reportUrl: string) {
  const response = await fetchAppApi("summaries", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      reportUrl,
      returnTo: window.location.pathname + window.location.search,
    }),
  });
  const result = await response.json();

  if (result.status === "pending_consent" && result.authorizationUrl) {
    window.location.href = result.authorizationUrl;
    return;
  }

  if (result.status === "queued") {
    // Payment has already settled for this idempotency key; show queued state.
    return;
  }

  if (result.status === "declined") {
    // The visitor declined this idempotent request.
    return;
  }

  if (result.status === "error") {
    throw new Error(result.error);
  }
}
```

`pending_consent` is the normal first response for a new request. Send the user
to `authorizationUrl` so they can pay or decline. `queued` usually means a retry
hit an already-settled request and the action dispatch is queued. `declined`
means the matching request was declined. `error` covers invalid args, an unknown
`actionName`, missing visitor favor auth, or a favor service failure.

After the visitor pays or declines, Rome Cloud sends them back to the
`returnTo` page (or `/apps/<appId>` when you didn't pass one) with
`favor=settled` or `favor=declined` appended as a query param — read it on
mount to show the outcome.

## Dispatch behavior [#dispatch-behavior]

When payment settles, Rome marks the request queued. The owner's instance syncs
queued favor requests, claims one, runs the requested action with the validated
args, and reports success or action failure back to Rome Cloud.

The dispatched action is a normal Rome action. Return `{ status: "ok", data }`
for success. Returning `{ status: "error", error }`, throwing, or returning a
suspended result marks the favor dispatch as failed.