Skip to main content

everruns_core/
payment.rs

1//! Machine payment DTOs and internal execution contract.
2//!
3//! Design decision: payment is an internal authority consumed by capabilities,
4//! not a generic model-facing paid HTTP tool. Domain tools such as
5//! `parallel_search` build typed requests and let the platform resolve wallets,
6//! enforce policy, sign, settle, and record receipts.
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11use crate::typed_id::{PaymentAccountId, PaymentAttemptId, PaymentPolicyId};
12
13#[cfg(feature = "openapi")]
14use utoipa::ToSchema;
15
16/// Payment rail used to settle a machine payment.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18#[cfg_attr(feature = "openapi", derive(ToSchema))]
19#[serde(rename_all = "snake_case")]
20pub enum PaymentRail {
21    MppTempo,
22    X402Base,
23}
24
25impl PaymentRail {
26    pub fn as_wire(&self) -> &'static str {
27        match self {
28            PaymentRail::MppTempo => "mpp_tempo",
29            PaymentRail::X402Base => "x402_base",
30        }
31    }
32}
33
34impl std::fmt::Display for PaymentRail {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.write_str(self.as_wire())
37    }
38}
39
40impl std::str::FromStr for PaymentRail {
41    type Err = String;
42
43    fn from_str(value: &str) -> Result<Self, Self::Err> {
44        match value {
45            "mpp_tempo" => Ok(PaymentRail::MppTempo),
46            "x402_base" => Ok(PaymentRail::X402Base),
47            _ => Err(format!("Invalid payment rail: {value}")),
48        }
49    }
50}
51
52impl From<&str> for PaymentRail {
53    fn from(value: &str) -> Self {
54        match value {
55            "x402_base" => PaymentRail::X402Base,
56            _ => PaymentRail::MppTempo,
57        }
58    }
59}
60
61/// Principal class that owns a payment account.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[cfg_attr(feature = "openapi", derive(ToSchema))]
64#[serde(rename_all = "snake_case")]
65pub enum PaymentOwnerType {
66    User,
67    AgentIdentity,
68    Organization,
69}
70
71impl PaymentOwnerType {
72    pub fn as_wire(&self) -> &'static str {
73        match self {
74            PaymentOwnerType::User => "user",
75            PaymentOwnerType::AgentIdentity => "agent_identity",
76            PaymentOwnerType::Organization => "organization",
77        }
78    }
79}
80
81impl std::fmt::Display for PaymentOwnerType {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.write_str(self.as_wire())
84    }
85}
86
87impl From<&str> for PaymentOwnerType {
88    fn from(value: &str) -> Self {
89        match value {
90            "agent_identity" => PaymentOwnerType::AgentIdentity,
91            "organization" => PaymentOwnerType::Organization,
92            _ => PaymentOwnerType::User,
93        }
94    }
95}
96
97/// Lifecycle state of a payment account, policy, or attempt. The shared
98/// vocabulary keeps account/policy admin and attempt settlement on the
99/// same status taxonomy.
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101#[cfg_attr(feature = "openapi", derive(ToSchema))]
102#[serde(rename_all = "snake_case")]
103pub enum PaymentStatus {
104    Active,
105    Disabled,
106    Pending,
107    Succeeded,
108    Failed,
109    Released,
110}
111
112impl PaymentStatus {
113    pub fn as_wire(&self) -> &'static str {
114        match self {
115            PaymentStatus::Active => "active",
116            PaymentStatus::Disabled => "disabled",
117            PaymentStatus::Pending => "pending",
118            PaymentStatus::Succeeded => "succeeded",
119            PaymentStatus::Failed => "failed",
120            PaymentStatus::Released => "released",
121        }
122    }
123}
124
125impl std::fmt::Display for PaymentStatus {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.write_str(self.as_wire())
128    }
129}
130
131impl From<&str> for PaymentStatus {
132    fn from(value: &str) -> Self {
133        match value {
134            "disabled" => PaymentStatus::Disabled,
135            "pending" => PaymentStatus::Pending,
136            "succeeded" => PaymentStatus::Succeeded,
137            "failed" => PaymentStatus::Failed,
138            "released" => PaymentStatus::Released,
139            _ => PaymentStatus::Active,
140        }
141    }
142}
143
144/// A payment account — the org-scoped source of funds for paid agent calls.
145/// Each account binds an owning principal (user, agent identity, or org)
146/// to one settlement rail and tracks its provisioning lifecycle.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[cfg_attr(feature = "openapi", derive(ToSchema))]
149pub struct PaymentAccount {
150    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
151    pub id: PaymentAccountId,
152    /// Owning organization's prefixed public identifier.
153    #[cfg_attr(
154        feature = "openapi",
155        schema(example = "org_01933b5a000070008000000000000001")
156    )]
157    pub organization_id: String,
158    /// Principal class that owns this account (user, agent identity, or organization).
159    pub owner_type: PaymentOwnerType,
160    /// Prefixed identifier of the owning principal (e.g. `user_…`, `agent_…`, `org_…`).
161    #[cfg_attr(
162        feature = "openapi",
163        schema(example = "agent_01933b5a000070008000000000000001")
164    )]
165    pub owner_id: String,
166    /// Settlement rail this account operates on.
167    pub rail: PaymentRail,
168    /// Human-readable label for this account. Safe to render in user-facing messages.
169    #[cfg_attr(feature = "openapi", schema(example = "Production USDC ops wallet"))]
170    pub label: String,
171    /// Public address on the rail (chain address, account number, etc.). Optional; `None` until provisioning completes.
172    #[cfg_attr(
173        feature = "openapi",
174        schema(example = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
175    )]
176    pub public_address: Option<String>,
177    /// Current lifecycle status of this account.
178    pub status: PaymentStatus,
179    /// Free-form metadata attached to this account (caller-defined; opaque to the platform).
180    #[cfg_attr(feature = "openapi", schema(example = json!({"team": "finance"})))]
181    pub metadata: serde_json::Value,
182    /// Timestamp when this account was created (RFC 3339).
183    #[cfg_attr(feature = "openapi", schema(example = "2026-04-01T10:00:00Z"))]
184    pub created_at: DateTime<Utc>,
185    /// Timestamp when this account was last updated (RFC 3339).
186    #[cfg_attr(feature = "openapi", schema(example = "2026-05-20T14:00:00Z"))]
187    pub updated_at: DateTime<Utc>,
188}
189
190/// A payment policy — the binding between a paying account and a subject
191/// (agent identity, session) that controls which paid calls are
192/// authorized and at what spend caps.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[cfg_attr(feature = "openapi", derive(ToSchema))]
195pub struct PaymentPolicy {
196    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
197    pub id: PaymentPolicyId,
198    /// Owning organization's prefixed public identifier.
199    #[cfg_attr(
200        feature = "openapi",
201        schema(example = "org_01933b5a000070008000000000000001")
202    )]
203    pub organization_id: String,
204    /// Payment account this policy authorizes spending from.
205    pub payment_account_id: PaymentAccountId,
206    /// Class of subject this policy binds to (e.g. `agent_identity`, `session`).
207    #[cfg_attr(feature = "openapi", schema(example = "agent_identity"))]
208    pub subject_type: String,
209    /// Prefixed identifier of the bound subject.
210    #[cfg_attr(
211        feature = "openapi",
212        schema(example = "identity_01933b5a000070008000000000000001")
213    )]
214    pub subject_id: String,
215    /// Capability IDs this policy permits paid calls for. Empty list means no capability gating.
216    #[cfg_attr(feature = "openapi", schema(example = json!(["paid_search", "paid_image_gen"])))]
217    pub allowed_capabilities: Vec<String>,
218    /// HTTP host allowlist for paid outbound calls. Empty list means no host gating.
219    #[cfg_attr(feature = "openapi", schema(example = json!(["api.openai.com", "api.anthropic.com"])))]
220    pub allowed_hosts: Vec<String>,
221    /// Preferred settlement rails in priority order; the authority picks the first available.
222    pub rail_preference: Vec<PaymentRail>,
223    /// Maximum amount (USD) any single paid request may settle for. **Enforced** by the payment authority at policy selection. `None` means no per-request cap.
224    #[cfg_attr(feature = "openapi", schema(example = 2.5))]
225    pub max_amount_usd_per_request: Option<f64>,
226    /// Maximum cumulative amount (USD) per agent turn. **Advisory only — not yet enforced.** Stored on the policy for forward compatibility; the payment authority currently checks only `max_amount_usd_per_request`. `None` means no per-turn cap.
227    #[cfg_attr(feature = "openapi", schema(example = 5.0))]
228    pub max_amount_usd_per_turn: Option<f64>,
229    /// Maximum cumulative amount (USD) per UTC day. **Advisory only — not yet enforced.** Stored on the policy for forward compatibility; the payment authority currently checks only `max_amount_usd_per_request`. `None` means no per-day cap.
230    #[cfg_attr(feature = "openapi", schema(example = 50.0))]
231    pub max_amount_usd_per_day: Option<f64>,
232    /// Threshold (USD) above which a request would require explicit human approval. **Advisory only — not yet enforced.** Stored on the policy for forward compatibility; no approval gate is wired up yet. `None` disables the (future) gate.
233    #[cfg_attr(feature = "openapi", schema(example = 10.0))]
234    pub require_approval_above_usd: Option<f64>,
235    /// Current lifecycle status of this policy.
236    pub status: PaymentStatus,
237    /// Free-form metadata attached to this policy.
238    #[cfg_attr(feature = "openapi", schema(example = json!({"created_by": "alex@acme.example"})))]
239    pub metadata: serde_json::Value,
240    /// Timestamp when this policy was created (RFC 3339).
241    #[cfg_attr(feature = "openapi", schema(example = "2026-04-01T10:00:00Z"))]
242    pub created_at: DateTime<Utc>,
243    /// Timestamp when this policy was last updated (RFC 3339).
244    #[cfg_attr(feature = "openapi", schema(example = "2026-05-20T14:00:00Z"))]
245    pub updated_at: DateTime<Utc>,
246}
247
248/// A single paid-call settlement attempt — the durable record of one
249/// authorization+settlement cycle issued through the payment authority.
250/// Persisted regardless of outcome so failed attempts remain auditable.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[cfg_attr(feature = "openapi", derive(ToSchema))]
253pub struct PaymentAttempt {
254    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
255    pub id: PaymentAttemptId,
256    /// Owning organization's prefixed public identifier.
257    #[cfg_attr(
258        feature = "openapi",
259        schema(example = "org_01933b5a000070008000000000000001")
260    )]
261    pub organization_id: String,
262    /// Payment account that settled (or attempted to settle) this attempt. `None` if no account could be resolved.
263    pub payment_account_id: Option<PaymentAccountId>,
264    /// Session that initiated the paid call, if any.
265    #[cfg_attr(
266        feature = "openapi",
267        schema(example = "session_01933b5a000070008000000000000001")
268    )]
269    pub session_id: Option<String>,
270    /// Capability ID that originated this paid call.
271    #[cfg_attr(feature = "openapi", schema(example = "paid_search"))]
272    pub capability: String,
273    /// Capability-specific operation name that originated this paid call.
274    #[cfg_attr(feature = "openapi", schema(example = "search.query"))]
275    pub operation: String,
276    /// Settlement rail actually used. `None` if the attempt failed before rail selection.
277    pub rail: Option<PaymentRail>,
278    /// Amount actually charged (USD).
279    #[cfg_attr(feature = "openapi", schema(example = 0.014))]
280    pub amount_usd: f64,
281    /// ISO 4217 currency code for the charge (typically `USD`).
282    #[cfg_attr(feature = "openapi", schema(example = "USD"))]
283    pub currency: String,
284    /// Destination URL of the paid outbound call.
285    #[cfg_attr(
286        feature = "openapi",
287        schema(example = "https://api.example.com/v1/search")
288    )]
289    pub target_url: String,
290    /// Stable hash of the outbound request used to detect replays. `None` when not applicable.
291    #[cfg_attr(
292        feature = "openapi",
293        schema(
294            example = "sha256:9f1e2a4c3d5b6e8a0b2c4d6e8f0a1b3c5d7e9f0a1b2c4d6e8f0a1b2c4d6e8f0a"
295        )
296    )]
297    pub request_hash: Option<String>,
298    /// Current lifecycle status of this attempt.
299    pub status: PaymentStatus,
300    /// Human-readable error message when `status` is `failed`; `None` otherwise.
301    #[cfg_attr(
302        feature = "openapi",
303        schema(example = "rail.insufficient_funds: settled balance below minimum")
304    )]
305    pub error_message: Option<String>,
306    /// Rail-specific receipt payload (transaction id, block reference, signature, etc.).
307    #[cfg_attr(feature = "openapi", schema(example = json!({"tx_hash": "0x4a1c2b3d", "block": 18234567})))]
308    pub receipt: serde_json::Value,
309    /// Timestamp when this attempt was created (RFC 3339).
310    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:00Z"))]
311    pub created_at: DateTime<Utc>,
312    /// Timestamp when this attempt was last updated (RFC 3339).
313    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:02Z"))]
314    pub updated_at: DateTime<Utc>,
315}
316
317/// HTTP method for an internal paid request.
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
319#[serde(rename_all = "UPPERCASE")]
320pub enum PaymentMethod {
321    Get,
322    Post,
323}
324
325impl PaymentMethod {
326    pub fn as_wire(&self) -> &'static str {
327        match self {
328            PaymentMethod::Get => "GET",
329            PaymentMethod::Post => "POST",
330        }
331    }
332}
333
334impl std::fmt::Display for PaymentMethod {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        f.write_str(self.as_wire())
337    }
338}
339
340impl std::str::FromStr for PaymentMethod {
341    type Err = String;
342
343    fn from_str(value: &str) -> Result<Self, Self::Err> {
344        match value {
345            "GET" => Ok(PaymentMethod::Get),
346            "POST" => Ok(PaymentMethod::Post),
347            _ => Err(format!("Invalid payment method: {value}")),
348        }
349    }
350}
351
352/// Internal request from a capability to the payment authority.
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct MachinePaymentRequest {
355    pub capability: String,
356    pub operation: String,
357    pub method: PaymentMethod,
358    pub url: String,
359    pub body: Option<serde_json::Value>,
360    pub max_amount_usd: f64,
361    pub rail_preference: Vec<PaymentRail>,
362    pub metadata: serde_json::Value,
363}
364
365/// Response returned to the calling capability after payment and execution.
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct MachinePaymentResponse {
368    pub attempt_id: Option<PaymentAttemptId>,
369    pub amount_usd: f64,
370    pub rail: Option<PaymentRail>,
371    pub response: serde_json::Value,
372    pub receipt: serde_json::Value,
373}