Skip to main content

loop_agent_sdk/
perception.rs

1//! Loop Agent SDK - Perception Interface
2//! 
3//! Event-driven triggers that wake up Lambda agents.
4//! Uses AWS EventBridge as the "central nervous system."
5
6use serde::{Deserialize, Serialize};
7use solana_sdk::pubkey::Pubkey;
8
9/// Event types that can trigger agent execution
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "type", content = "payload")]
12pub enum PerceptionEvent {
13    /// Transaction detected from POS integration (Square/Stripe/Clover)
14    TransactionDetected(TransactionEvent),
15    
16    /// User entered geofenced merchant location
17    LocationHit(LocationEvent),
18    
19    /// ZK proof submitted by user (manual capture)
20    ProofSubmitted(ProofEvent),
21    
22    /// Staking position approaching unlock
23    PositionUnlocking(UnlockEvent),
24    
25    /// Yield threshold reached (for auto-compound)
26    YieldThreshold(YieldEvent),
27    
28    /// Scheduled optimization check
29    ScheduledCheck(ScheduledEvent),
30}
31
32/// Transaction from POS webhook
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TransactionEvent {
35    /// Source processor
36    pub processor: Processor,
37    /// Processor's transaction ID
38    pub processor_txn_id: String,
39    /// Merchant pubkey in Loop registry
40    pub merchant_id: String,
41    /// Location ID if multi-location merchant
42    pub location_id: Option<String>,
43    /// Amount in cents
44    pub amount_cents: u64,
45    /// Card last 4 for matching
46    pub card_last4: String,
47    /// Card brand
48    pub card_brand: String,
49    /// Card fingerprint for dedup
50    pub card_fingerprint: String,
51    /// When transaction occurred
52    pub occurred_at: i64,
53    /// Raw webhook payload for audit
54    pub raw_payload: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
58pub enum Processor {
59    Square,
60    Stripe,
61    Clover,
62    Fidel,
63}
64
65/// User location event from mobile app
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct LocationEvent {
68    /// User's pubkey
69    pub user_pubkey: String,
70    /// Merchant they're near
71    pub merchant_id: String,
72    /// Location coordinates
73    pub latitude: f64,
74    pub longitude: f64,
75    /// Accuracy in meters
76    pub accuracy_m: f32,
77    /// Entry or exit
78    pub event_type: GeoEventType,
79    /// Timestamp
80    pub timestamp: i64,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
84pub enum GeoEventType {
85    Enter,
86    Exit,
87    Dwell,  // Stayed > 5 minutes
88}
89
90/// User-submitted proof event
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ProofEvent {
93    pub user_pubkey: String,
94    pub proof_type: String,  // "reclaim", "zktls", etc.
95    pub proof_data: String,  // Base64 encoded
96    pub claimed_amount_cents: u64,
97    pub claimed_merchant: Option<String>,
98    pub submitted_at: i64,
99}
100
101/// Staking position unlocking soon
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct UnlockEvent {
104    pub user_pubkey: String,
105    pub position_id: String,
106    pub unlock_time: i64,
107    pub amount: u64,
108    pub accrued_yield: u64,
109    /// Hours until unlock
110    pub hours_until_unlock: u16,
111}
112
113/// Yield threshold reached
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct YieldEvent {
116    pub user_pubkey: String,
117    pub total_pending_yield: u64,
118    pub threshold_reached: u64,
119    pub positions: Vec<String>,
120}
121
122/// Scheduled check (cron-triggered)
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ScheduledEvent {
125    pub check_type: ScheduledCheckType,
126    pub batch_id: String,
127    pub user_pubkeys: Vec<String>,  // Batch of users to process
128}
129
130#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
131pub enum ScheduledCheckType {
132    /// Daily yield optimization check
133    DailyOptimization,
134    /// Weekly position health check
135    WeeklyHealth,
136    /// Process pending captures
137    PendingCaptures,
138}
139
140/// EventBridge rule configuration
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EventBridgeRule {
143    pub name: String,
144    pub event_pattern: serde_json::Value,
145    pub target_lambda: String,
146    pub description: String,
147}
148
149/// Generate EventBridge rules for Loop agent
150pub fn generate_eventbridge_rules(lambda_arn: &str) -> Vec<EventBridgeRule> {
151    vec![
152        EventBridgeRule {
153            name: "loop-transaction-detected".to_string(),
154            event_pattern: serde_json::json!({
155                "source": ["loop.pos"],
156                "detail-type": ["TransactionDetected"]
157            }),
158            target_lambda: lambda_arn.to_string(),
159            description: "Trigger agent on POS transaction webhook".to_string(),
160        },
161        EventBridgeRule {
162            name: "loop-location-hit".to_string(),
163            event_pattern: serde_json::json!({
164                "source": ["loop.mobile"],
165                "detail-type": ["LocationHit"]
166            }),
167            target_lambda: lambda_arn.to_string(),
168            description: "Trigger agent on user geofence entry".to_string(),
169        },
170        EventBridgeRule {
171            name: "loop-proof-submitted".to_string(),
172            event_pattern: serde_json::json!({
173                "source": ["loop.capture"],
174                "detail-type": ["ProofSubmitted"]
175            }),
176            target_lambda: lambda_arn.to_string(),
177            description: "Trigger agent on ZK proof submission".to_string(),
178        },
179        EventBridgeRule {
180            name: "loop-position-unlocking".to_string(),
181            event_pattern: serde_json::json!({
182                "source": ["loop.staking"],
183                "detail-type": ["PositionUnlocking"]
184            }),
185            target_lambda: lambda_arn.to_string(),
186            description: "Notify agent 24h before position unlock".to_string(),
187        },
188        EventBridgeRule {
189            name: "loop-yield-threshold".to_string(),
190            event_pattern: serde_json::json!({
191                "source": ["loop.staking"],
192                "detail-type": ["YieldThreshold"]
193            }),
194            target_lambda: lambda_arn.to_string(),
195            description: "Trigger auto-compound when yield threshold reached".to_string(),
196        },
197        EventBridgeRule {
198            name: "loop-daily-optimization".to_string(),
199            event_pattern: serde_json::json!({
200                "source": ["aws.scheduler"],
201                "detail-type": ["Scheduled Event"],
202                "detail": {
203                    "check_type": ["DailyOptimization"]
204                }
205            }),
206            target_lambda: lambda_arn.to_string(),
207            description: "Daily yield optimization batch (6 AM UTC)".to_string(),
208        },
209    ]
210}
211
212/// Trait for perception handlers
213/// Implement this in your Lambda to handle events
214pub trait PerceptionHandler {
215    /// Handle incoming perception event
216    /// Returns true if action was taken
217    fn on_event(&self, event: PerceptionEvent) -> Result<bool, PerceptionError>;
218    
219    /// Filter events before processing (cost optimization)
220    fn should_process(&self, event: &PerceptionEvent) -> bool {
221        true  // Default: process all
222    }
223}
224
225#[derive(Debug, Clone)]
226pub enum PerceptionError {
227    /// Event data malformed
228    InvalidEvent(String),
229    /// User not found in system
230    UserNotFound,
231    /// Merchant not found
232    MerchantNotFound,
233    /// Duplicate event (already processed)
234    DuplicateEvent,
235    /// Rate limited
236    RateLimited,
237    /// Internal error
238    Internal(String),
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn can_deserialize_transaction_event() {
247        let json = r#"{
248            "type": "TransactionDetected",
249            "payload": {
250                "processor": "Square",
251                "processor_txn_id": "abc123",
252                "merchant_id": "5A7E...YKTZ",
253                "location_id": null,
254                "amount_cents": 1599,
255                "card_last4": "1234",
256                "card_brand": "VISA",
257                "card_fingerprint": "fp_abc123",
258                "occurred_at": 1711468800,
259                "raw_payload": {}
260            }
261        }"#;
262        
263        let event: PerceptionEvent = serde_json::from_str(json).unwrap();
264        match event {
265            PerceptionEvent::TransactionDetected(tx) => {
266                assert_eq!(tx.amount_cents, 1599);
267            }
268            _ => panic!("Wrong event type"),
269        }
270    }
271}