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: "a-random-secret-of-at-least-32-bytes",
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 to42431(Moderato testnet)"fee_payer"— (optional) enable server-side fee sponsorship, defaults tofalse. Whentrue, the server co-signs client transactions with domain0x78to pay transaction fees. Requires"fee_payer_private_key"and"fee_token", unless"fee_payer_url"is set for hosted sponsorship. Sponsorship also requires an explicitly selected atomic"store"."fee_payer_url"— (optional) URL of a hosted fee-payer service that implementseth_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."sponsor_budget_id"— required with"fee_payer_url"; stable identity shared by every endpoint using the same hosted sponsor wallet. It is normalized before use and should normally be that wallet's address."fee_payer_private_key"— (required whenfee_payer: trueand nofee_payer_url) hex-encoded 32-byte secp256k1 private key for the fee payer account"fee_token"— (required for local co-sign whenfee_payer: trueand nofee_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_tokensor per-chain defaults)."fee_payer_allowed_fee_tokens"— (optional,fee_payer: trueonly) list of hex addresses overriding the default sponsor fee-token allowlist."fee_payer_policy"— (optional,fee_payer: trueonly) 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. The same map accepts"max_in_flight_total_fee"and"max_in_flight_reservations"for aggregate worst-case exposure. SeeMPP.Methods.Tempo.FeePayerPolicy."sponsor_budget_reconcile"— (optional, defaults tofalse) whentrue, an at-capacity request checks a bounded set of pending async transaction receipts before rejecting."memo"— (optional) bytes32 hex memo fortransferWithMemo"wait_for_confirmation"— (optional) whenfalse, broadcasts without waiting for on-chain confirmation. Pre-simulates the full co-signed transaction viaeth_simulateV1first (same guard as the default path) to reject a tx that would revert before broadcast, then broadcasts async and returns an optimistic receipt. Defaulttrue."store"— (optional) replay-dedup store. On by default — when absent, the app-startedMPP.Tempo.ConCacheStoreis used so replay protection is enabled out of the box (issue #7). Pass a module implementingMPP.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 atomiccheck_and_mark/2. Passstore: falseto explicitly opt out of dedup (not recommended; incompatible with a static"memo"). Fee sponsorship is stricter: the store must be selected explicitly and implement atomicupdate/3.MPP.Tempo.ConCacheStoreprovides a single-node bound. Every node and endpoint sponsoring the same wallet must use the same physical shared atomic backend for a cluster-wide bound."machine_token_enabled"— (optional) advertise and verify first-party machine-token (MPP Credits / machineUSD) charge payments. Whentrue, 402 method details include"machineTokenEnabled" => true, hash receipts may settle from the canonical swapper, andtype="transaction"credentials may match the exact[approve, swapTo]route. Supported only on Tempo mainnet (4217) and Moderato (42431). Defaults tofalse."require_presenter_binding"— (optional) requiretype="hash"andtype="transaction"credential presenters to prove control of the transfer's sender wallet via a"presenterSignature"payload field (see Presenter binding below). Defaults tofalse."supported_modes"— (optional) non-empty list of"pull"and/or"push". Advertised asmethodDetails.supportedModes."pull"istype="transaction"(server broadcasts);"push"istype="hash"(client already broadcast). When set, a credential whose mode is not listed is rejected before RPC. Omitted (the default) allows both modes. Zero-amounttype="proof"credentials ignore this field (draft-tempo-charge-00; mpp-rs #455).
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).
Recurring subscriptions
Routes configured with intent: "subscription" use the shared
MPP.Intents.Subscription schema and delegate activation and renewal to
MPP.Methods.Tempo.Subscription. Subscription configuration requires
"rpc_url", "chain_id", and "subscription_access_key_private_key".
It accepts a "subscription_store" implementing MPP.Subscription.Store;
the application-started MPP.Subscription.ETSStore is the single-node
default. Subscription fee sponsorship supports the local fee-payer path,
not "fee_payer_url".
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-levelsource(adid:pkh:eip155:DID) is required and names the account; the signature must recover to it, and the matched transfer'sfrommust equal it (or, when"machine_token_enabled"is set, equal the canonical swapper while the transaction sender equals it).type="transaction"— the account is the sender recovered from the signed transaction itself; asource, 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, machineTokenEnabled, presenterBinding, supportedModes) 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 |
credential_types | 0 | Return the Tempo charge payload types: hash, transaction, and proof. | - |
method_name | 0 | Return the payment method identifier for Tempo. | - |
Summary
Functions
Return Tempo-specific fields (chainId, feePayer, memo, machineTokenEnabled, presenterBinding, supportedModes) for the 402 challenge.
Return the Tempo charge payload types: hash, transaction, and proof.
Return the payment method identifier for Tempo.
Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies.
Verify a Tempo credential by checking on-chain settlement.
Functions
@spec challenge_method_details(MPP.Method.intent()) :: map() | nil
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map()
@spec challenge_method_details(MPP.Intents.Subscription.t()) :: map()
Return Tempo-specific fields (chainId, feePayer, memo, machineTokenEnabled, presenterBinding, supportedModes) for the 402 challenge.
Parameters
charge- Charge struct with method_details containingchain_id,fee_payer, and optionallymemo/supported_modes(value)
Returns
Map with chainId (default 42431), feePayer (default false), optional memo, machineTokenEnabled (present and true only when enabled), presenterBinding (present and true only when required), and supportedModes (present only when configured) (map)
# descripex:contract
%{
params: %{
charge: %{
description: "Charge struct with method_details containing `chain_id`, `fee_payer`, and optionally `memo` / `supported_modes`",
kind: :value
}
},
returns: %{
type: :map,
description: "Map with `chainId` (default 42431), `feePayer` (default false), optional `memo`, `machineTokenEnabled` (present and `true` only when enabled), `presenterBinding` (present and `true` only when required), and `supportedModes` (present only when configured)"
}
}
Return the Tempo charge payload types: hash, transaction, and proof.
# descripex:contract
%{}
@spec method_name() :: String.t()
Return the payment method identifier for Tempo.
# descripex:contract
%{}
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)
# descripex:contract
%{
params: %{
config: %{description: "method_config map to validate", kind: :value}
},
returns: %{
type: :atom,
description: "`:ok` on success, raises `ArgumentError` on missing keys"
}
}
@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.Subscription.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 (includingrpc_url) (value)
Returns
{:ok, receipt} on success, {:error, error} on failure (tagged_tuple)
Errors
:invalid_payload:verification_failed:sponsor_capacity_exhausted
# descripex:contract
%{
params: %{
charge: %{
description: "Charge intent struct with amount, currency, and method_details (including `rpc_url`)",
kind: :value
},
payload: %{
description: "Credential payload map with `\"type\"` (`\"hash\"` or `\"transaction\"`) and corresponding proof field",
kind: :value
}
},
errors: [:invalid_payload, :verification_failed, :sponsor_capacity_exhausted],
returns: %{
type: :tagged_tuple,
description: "`{:ok, receipt}` on success, `{:error, error}` on failure"
}
}