# `MPP.Methods.Tempo`
[🔗](https://github.com/ZenHive/mpp/blob/v0.10.0/lib/mpp/methods/tempo.ex#L1)

Tempo payment method — verifies payment via on-chain TIP-20 token transfer.

Tempo supports two credential types:

  * `type="hash"` — Client already broadcast the transaction; server verifies
    the receipt via RPC (`eth_getTransactionReceipt`).
  * `type="transaction"` — Client sends a signed Tempo Transaction (0x76);
    server decodes, optionally adds fee payer signature, broadcasts, and verifies.

## Configuration

Pass Tempo-specific config via `:method_config` in `MPP.Plug` opts:

    plug MPP.Plug,
      secret_key: "hmac-secret",
      realm: "api.example.com",
      method: MPP.Methods.Tempo,
      amount: "1000000",
      currency: "0x20c0000000000000000000000000000000000000",
      method_config: %{
        "rpc_url" => "https://rpc.moderato.tempo.xyz",
        "chain_id" => 42431,
        "fee_payer" => false
      }

## Config Keys

  * `"rpc_url"` — (required) Tempo RPC endpoint URL
  * `"chain_id"` — (optional) network chain ID, defaults to `42431` (Moderato testnet)
  * `"fee_payer"` — (optional) enable server-side fee sponsorship, defaults to `false`.
    When `true`, the server co-signs client transactions with domain `0x78` to pay
    transaction fees. Requires `"fee_payer_private_key"` and `"fee_token"`, unless
    `"fee_payer_url"` is set for hosted sponsorship.
  * `"fee_payer_url"` — (optional) URL of a hosted fee-payer service that implements
    `eth_fillTransaction`. When set, the server delegates co-signing to the remote
    endpoint instead of using `"fee_payer_private_key"`. Mutually exclusive with the
    local key path.
  * `"fee_payer_private_key"` — (required when `fee_payer: true` and no `fee_payer_url`)
    hex-encoded 32-byte
    secp256k1 private key for the fee payer account
  * `"fee_token"` — (required for local co-sign when `fee_payer: true` and no
    `fee_payer_url`) hex address of a USD-denominated
    TIP-20 token to use for fee payment (e.g., pathUSD). Must be on the sponsor
    allowlist (`fee_payer_allowed_fee_tokens` or per-chain defaults).
  * `"fee_payer_allowed_fee_tokens"` — (optional, `fee_payer: true` only) list of
    hex addresses overriding the default sponsor fee-token allowlist.
  * `"fee_payer_policy"` — (optional, `fee_payer: true` only) map of sponsor
    ceilings overriding the per-chain defaults: `"max_gas"`,
    `"max_fee_per_gas"`, `"max_priority_fee_per_gas"`, `"max_total_fee"` (wei),
    and `"max_validity_window_seconds"` (seconds). Bounds the client-supplied
    gas fields and validity window before the server co-signs so a malicious
    client cannot drain the fee-payer wallet via inflated gas price, total fee
    budget, or a padded access list, nor hold a co-signed sponsorship
    broadcastable far into the future. See `MPP.Methods.Tempo.FeePayerPolicy`.
  * `"memo"` — (optional) bytes32 hex memo for `transferWithMemo`
  * `"wait_for_confirmation"` — (optional) when `false`, broadcasts without waiting
    for on-chain confirmation. Pre-simulates the full co-signed transaction via
    `eth_simulateV1` first (same guard as the default path) to reject a tx that would
    revert before broadcast, then broadcasts async and returns an optimistic receipt.
    Default `true`.
  * `"store"` — (optional) replay-dedup store. **On by default** — when absent, the
    app-started `MPP.Tempo.ConCacheStore` is used so replay protection is enabled out of
    the box (issue #7). Pass a module implementing `MPP.Tempo.Store` (Redis,
    Postgres, etc. — for multi-node deployments) or `{MPP.Tempo.ConCacheStore, opts}` to
    configure the built-in store (for example a custom cache `:name`). A configured store
    MUST implement the atomic `check_and_mark/2`. Pass `store: false` to explicitly opt out
    of dedup (not recommended; incompatible with a static `"memo"`).
  * `"require_presenter_binding"` — (optional) require `type="hash"` and
    `type="transaction"` credential presenters to prove control of the transfer's
    sender wallet via a `"presenterSignature"` payload field (see *Presenter
    binding* below). Defaults to `false`.

## Credential Payload

The credential `payload` map must contain one of:

  * `"type" => "hash"`, `"hash" => "0x..."` — transaction hash for receipt verification
  * `"type" => "transaction"`, `"signature" => "..."` — RLP-serialized signed Tempo Transaction

Both may additionally carry `"presenterSignature" => "0x..."` (see below).

## Presenter Binding

On the hash/transaction paths, dedup is keyed on the tx hash alone — nothing in the
base protocol proves the credential *presenter* controls the wallet that broadcast
the transfer, so a third party who observes a settled transfer can race its hash
against their own fresh challenge (the residual documented in GHSA-34g7-vx6g-82mq).
Setting `"require_presenter_binding" => true` closes that race: the presenter must
include `"presenterSignature"`, an EIP-712 signature over the same `Proof` typed
data the `type="proof"` path uses (domain `{name: "MPP", version: "3", chainId}`,
struct `{account, challengeId, realm}` — see `MPP.Methods.Tempo.Proof`), signed by
the transfer sender's wallet or one of its authorized access keys.

  * `type="hash"` — the credential's top-level `source` (a `did:pkh:eip155:` DID)
    is required and names the account; the signature must recover to it, and the
    matched transfer's `from` must equal it.
  * `type="transaction"` — the account is the sender recovered from the signed
    transaction itself; a `source`, when present, must match it.

When the flag is off (default) a supplied `"presenterSignature"` is still verified
(an invalid one is rejected), so compliant clients can send it unconditionally.
The requirement is advertised to clients as `"presenterBinding" => true` in the
402 challenge's method details.

This is a deliberate hardening extension beyond the reference SDKs: neither mpp-rs
nor mppx binds the presenter on the hash path (both default the expected sender to
the receipt's `from` — `refs/mpp-rs/src/protocol/methods/tempo/method.rs` `verify_hash`,
`refs/mppx/src/tempo/server/Charge.ts` hash branch), which is why it is opt-in.

## Dependencies

Requires the `onchain` and `onchain_tempo` packages for RPC calls, transfer log
parsing, and 0x76 Tempo transaction handling.

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `challenge_method_details` | 1 | Return Tempo-specific fields (`chainId`, `feePayer`, `memo`, `presenterBinding`) for the 402 challenge. | `charge: value` |
| `verify` | 2 | Verify a Tempo credential by checking on-chain settlement. | `payload: value`, `charge: value` |
| `validate_config!` | 1 | Validate Tempo method_config at init time. Raises on missing `rpc_url` or unavailable `onchain` / `onchain_tempo` dependencies. | `config: value` |
| `method_name` | 0 | Return the payment method identifier for Tempo. | - |

# `challenge_method_details`

```elixir
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map() | nil
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map()
```

Return Tempo-specific fields (`chainId`, `feePayer`, `memo`, `presenterBinding`) for the 402 challenge.

## Parameters

  * `charge` - Charge struct with method_details containing `chain_id`, `fee_payer`, and optionally `memo` (value)

## Returns

Map with `chainId` (default 42431), `feePayer` (default false), optional `memo`, and `presenterBinding` (present and `true` only when required) (`map`)

```elixir
# descripex:contract
%{
  params: %{
    charge: %{
      description: "Charge struct with method_details containing `chain_id`, `fee_payer`, and optionally `memo`",
      kind: :value
    }
  },
  returns: %{
    type: :map,
    description: "Map with `chainId` (default 42431), `feePayer` (default false), optional `memo`, and `presenterBinding` (present and `true` only when required)"
  }
}
```

# `method_name`

```elixir
@spec method_name() :: String.t()
```

Return the payment method identifier for Tempo.

```elixir
# descripex:contract
%{}
```

# `validate_config!`

```elixir
@spec validate_config!(map()) :: :ok
@spec validate_config!(map()) :: :ok
```

Validate Tempo method_config at init time. Raises on missing `rpc_url` or unavailable `onchain` / `onchain_tempo` dependencies.

## Parameters

  * `config` - method_config map to validate (value)

## Returns

`:ok` on success, raises `ArgumentError` on missing keys (`atom`)

```elixir
# descripex:contract
%{
  params: %{
    config: %{description: "method_config map to validate", kind: :value}
  },
  returns: %{
    type: :atom,
    description: "`:ok` on success, raises `ArgumentError` on missing keys"
  }
}
```

# `verify`

```elixir
@spec verify(map(), MPP.Intents.Charge.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) ::
  {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) :: {:error, MPP.Errors.t()}
```

Verify a Tempo credential by checking on-chain settlement.

## Parameters

  * `payload` - Credential payload map with `"type"` (`"hash"` or `"transaction"`) and corresponding proof field (value)
  * `charge` - Charge intent struct with amount, currency, and method_details (including `rpc_url`) (value)

## Returns

`{:ok, receipt}` on success, `{:error, error}` on failure (`tagged_tuple`)

## Errors

  * `:invalid_payload`
  * `:verification_failed`

```elixir
# descripex:contract
%{
  params: %{
    payload: %{
      description: "Credential payload map with `\"type\"` (`\"hash\"` or `\"transaction\"`) and corresponding proof field",
      kind: :value
    },
    charge: %{
      description: "Charge intent struct with amount, currency, and method_details (including `rpc_url`)",
      kind: :value
    }
  },
  errors: [:invalid_payload, :verification_failed],
  returns: %{
    type: :tagged_tuple,
    description: "`{:ok, receipt}` on success, `{:error, error}` on failure"
  }
}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
