CKT commited on
Commit
c5f454e
·
1 Parent(s): 62a350d

AgentPay integrated

Browse files
.gitignore CHANGED
@@ -3,3 +3,4 @@ _context/
3
  .DS_Store
4
  .env
5
  .gradio/
 
 
3
  .DS_Store
4
  .env
5
  .gradio/
6
+ __pycache__/
agentpay_sdk/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Expose the client class at the package level
2
+ from .client import AgentPayClient
3
+
4
+ # Optionally define __all__ to control wildcard imports
5
+ __all__ = ["AgentPayClient"]
agentpay_sdk/client.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Simple client to interact with AgentHub API
2
+
3
+ import requests
4
+ from typing import Optional, Dict, Any
5
+ import json # For decoding JSON response
6
+
7
+ from .types import ConsumptionResult
8
+ from .exceptions import AgentPayAPIError, AgentPayConnectionError # Import custom exceptions
9
+
10
+ API_BASE_URL = "https://api.agentpay.me"
11
+ API_VERSION_PREFIX = "/v1"
12
+
13
+ class AgentPayClient:
14
+ """Client for interacting with the AgentPay API.
15
+
16
+ Handles authentication using a Service Token and provides methods
17
+ for validating user API keys and consuming balance.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ service_token: str,
23
+ api_url: Optional[str] = None,
24
+ ):
25
+ """Initializes the AgentPay Client.
26
+
27
+ Args:
28
+ service_token: The unique token issued to your service by AgentHub.
29
+ api_url: Optional. Manually override the base API URL.
30
+ """
31
+ if not service_token:
32
+ raise ValueError("service_token is required.")
33
+
34
+ self.service_token = service_token
35
+
36
+ if api_url:
37
+ # Use manual override if provided
38
+ self.base_api_url = api_url.rstrip('/')
39
+ else:
40
+ self.base_api_url = API_BASE_URL
41
+
42
+ # Add the /api/v1 prefix
43
+ self.base_api_url += API_VERSION_PREFIX
44
+
45
+ # Prepare headers for requests
46
+ self.headers = {
47
+ "Authorization": f"Bearer {self.service_token}",
48
+ "Content-Type": "application/json",
49
+ "Accept": "application/json",
50
+ }
51
+
52
+ def _request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
53
+ """Internal helper to make requests to the AgentPay API.
54
+
55
+ Args:
56
+ method: HTTP method (e.g., "POST", "GET").
57
+ endpoint: API endpoint path (e.g., "/balances/consume").
58
+ data: Optional dictionary payload for POST/PUT/PATCH requests.
59
+
60
+ Returns:
61
+ The JSON response body as a dictionary.
62
+
63
+ Raises:
64
+ AgentPayConnectionError: If a network error occurs.
65
+ AgentPayAPIError: If the API returns an error status code.
66
+ """
67
+ full_url = self.base_api_url + endpoint
68
+
69
+ try:
70
+ response = requests.request(
71
+ method=method,
72
+ url=full_url,
73
+ headers=self.headers,
74
+ json=data, # requests library handles JSON serialization
75
+ timeout=10 # Add a reasonable timeout (e.g., 10 seconds)
76
+ )
77
+
78
+ # Raise exception for bad status codes (4xx or 5xx)
79
+ response.raise_for_status()
80
+
81
+ # Attempt to parse successful response as JSON
82
+ # Handle cases where response might be empty (e.g., 204 No Content)
83
+ if response.status_code == 204:
84
+ return {} # Return empty dict for No Content
85
+
86
+ return response.json()
87
+
88
+ except requests.exceptions.Timeout as e:
89
+ raise AgentPayConnectionError(f"Request timed out connecting to {full_url}: {e}") from e
90
+ except requests.exceptions.ConnectionError as e:
91
+ raise AgentPayConnectionError(f"Could not connect to {full_url}: {e}") from e
92
+ except requests.exceptions.HTTPError as e:
93
+ # Handle API errors (4xx, 5xx)
94
+ status_code = e.response.status_code
95
+ error_code = None
96
+ error_message = None
97
+ try:
98
+ # Attempt to parse error details from response body
99
+ error_data = e.response.json()
100
+ error_code = error_data.get("error_code")
101
+ error_message = error_data.get("error_message") or error_data.get("detail") # FastAPI uses detail sometimes
102
+ except json.JSONDecodeError:
103
+ # If response body is not JSON or empty
104
+ error_message = e.response.text or f"HTTP error {status_code}"
105
+
106
+ raise AgentPayAPIError(
107
+ status_code=status_code,
108
+ error_code=error_code,
109
+ error_message=error_message
110
+ ) from e
111
+ except json.JSONDecodeError as e:
112
+ # Handle cases where a 2xx response is not valid JSON
113
+ raise AgentPayAPIError(
114
+ status_code=response.status_code, # Use the status code from the original response
115
+ error_message=f"Failed to decode successful JSON response: {e}"
116
+ ) from e
117
+ except requests.exceptions.RequestException as e:
118
+ # Catch any other requests-related errors
119
+ raise AgentPayConnectionError(f"An unexpected network error occurred: {e}") from e
120
+
121
+ def consume(
122
+ self,
123
+ api_key: str,
124
+ amount_cents: int,
125
+ usage_event_id: str,
126
+ metadata: Optional[Dict[str, Any]] = None,
127
+ ) -> ConsumptionResult:
128
+ """Consumes balance for a user associated with an API key.
129
+
130
+ Args:
131
+ api_key: The User's API key to consume balance against.
132
+ amount_cents: The amount to consume in cents (must be >= 0).
133
+ usage_event_id: A unique identifier (e.g., UUID string) for this specific
134
+ consumption attempt, used for idempotency.
135
+ metadata: Optional dictionary containing additional data to associate
136
+ with the usage event.
137
+
138
+ Returns:
139
+ A ConsumptionResult object indicating success or failure.
140
+ """
141
+ if amount_cents < 0:
142
+ # Prevent making an API call with an invalid amount
143
+ # The API would reject this anyway, but better to catch early.
144
+ return ConsumptionResult(
145
+ success=False,
146
+ error_code="INVALID_INPUT",
147
+ error_message="Consumption amount cannot be negative."
148
+ )
149
+
150
+ endpoint = "/balances/consume"
151
+ payload = {
152
+ "api_key": api_key,
153
+ "amount_cents": amount_cents,
154
+ "usage_event_id": usage_event_id,
155
+ }
156
+ if metadata:
157
+ payload["metadata"] = metadata
158
+
159
+ try:
160
+ # _request raises AgentPayAPIError for 4xx/5xx responses
161
+ # and AgentPayConnectionError for network issues.
162
+ response_data = self._request("POST", endpoint, data=payload)
163
+
164
+ # If _request completes without raising an exception, it implies a 2xx response.
165
+ # The API's 200 OK response for consume just confirms success.
166
+ return ConsumptionResult(success=True)
167
+
168
+ except AgentPayAPIError as e:
169
+ # API returned a non-2xx status code (e.g., 402, 403, 409)
170
+ return ConsumptionResult(
171
+ success=False,
172
+ error_code=e.error_code,
173
+ error_message=e.error_message
174
+ )
175
+ except AgentPayConnectionError as e:
176
+ # Network error occurred during the request
177
+ return ConsumptionResult(
178
+ success=False,
179
+ error_code="NETWORK_ERROR",
180
+ error_message=str(e)
181
+ )
182
+ except Exception as e:
183
+ # Catch any other unexpected errors during the process
184
+ return ConsumptionResult(
185
+ success=False,
186
+ error_code="SDK_ERROR",
187
+ error_message=f"An unexpected error occurred in the SDK: {e}"
188
+ )
189
+
190
+ def validate_api_key(self, api_key: str) -> bool:
191
+ """Validates if a User API Key is active and valid for this service.
192
+
193
+ Args:
194
+ api_key: The User's API key to validate.
195
+
196
+ Returns:
197
+ True if the API key is valid for the service, False otherwise.
198
+ Returns False if any API or network error occurs during validation.
199
+ """
200
+ endpoint = "/api_keys/validate"
201
+ payload = {"api_key": api_key}
202
+
203
+ try:
204
+ response_data = self._request("POST", endpoint, data=payload)
205
+
206
+ # Expecting a response like {"is_valid": true | false}
207
+ is_valid = response_data.get("is_valid")
208
+
209
+ if isinstance(is_valid, bool):
210
+ return is_valid
211
+ else:
212
+ # Unexpected response format from the API
213
+ # Log this? For now, treat unexpected format as invalid.
214
+ print(f"Warning: Unexpected response format from /validate endpoint: {response_data}")
215
+ return False
216
+
217
+ except (AgentPayAPIError, AgentPayConnectionError) as e:
218
+ # If any API or network error occurs, treat the key as invalid for safety.
219
+ # Log the error for debugging if needed.
220
+ print(f"API or Connection Error during validation: {e}")
221
+ return False
222
+ except Exception as e:
223
+ # Catch any other unexpected errors
224
+ print(f"Unexpected SDK error during validation: {e}")
225
+ return False
agentpay_sdk/exceptions.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class AgentPayError(Exception):
2
+ """Base exception class for AgentPay SDK errors."""
3
+ pass
4
+
5
+ class AgentPayAPIError(AgentPayError):
6
+ """Raised when the AgentPay API returns an error response."""
7
+ def __init__(self, status_code: int, error_code: str | None = None, error_message: str | None = None):
8
+ self.status_code = status_code
9
+ self.error_code = error_code
10
+ self.error_message = error_message
11
+ detail = f"API returned status {status_code}"
12
+ if error_code:
13
+ detail += f" (Code: {error_code})"
14
+ if error_message:
15
+ detail += f": {error_message}"
16
+ super().__init__(detail)
17
+
18
+ class AgentPayConnectionError(AgentPayError):
19
+ """Raised when there is a network issue connecting to the AgentPay API."""
20
+ pass
agentpay_sdk/types.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ @dataclass
5
+ class ConsumptionResult:
6
+ """Represents the outcome of a balance consumption attempt."""
7
+ success: bool
8
+ error_code: Optional[str] = None
9
+ error_message: Optional[str] = None
app.py CHANGED
@@ -5,6 +5,28 @@ import uuid
5
  import os # Added for path joining
6
  import copy # For deep copying message list
7
  from datetime import datetime, timezone # Added timezone for UTC consistency
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  # --- Start of JSON I/O Helper Functions ---
10
 
@@ -102,8 +124,8 @@ def new_profile(request: gr.Request):
102
  # Decide how to handle this error - maybe return an error to the client?
103
 
104
  # 4. Return IDs, questionnaire data, and instructions
105
- instructions_for_agent = "You have received a `profile_id` (public identifier for this user\'s profile) and an `auth_id` (private key for authentication for this user). Please ask the user to store both securely. The `auth_id` must be sent as an `X-Auth-ID` header in subsequent requests that require authentication for this user (e.g., `update_profile_answers`, `get_matches`, `get_messages`, `get_profile` for own profile, `send_message`). The `profile_id` is used to publicly identify this user to others (e.g., in matches, or when sending/receiving messages). You must now walk the user through the questionnaire, preferably question by question, and collect the answers (in preparation to update the user's profile using update_profile_answers). If the user provides answers that are not in the list of valid answers, please ask them to provide a valid answer."
106
- instructions_for_user = "Your profile creation process has started! You\'ve been assigned a unique Profile ID and a secret Auth ID. Your AI agent will use these to manage your profile and interactions. Please walk through the questionnaire with your Agent, and provide your answers. You can upload a profile picture later. Before you proceed, please update your MCP configuration to include your Auth ID by adding the following to your mcp.json (or similar) configuration file: `\"matchmaker\": {\"command\": \"npx\", \"args\": [\"mcp-remote\", \"http://localhost:7860/gradio_api/mcp/sse\", \"--allow-http\", \"--header\", \"X-AUTH-ID:<your-auth-id>\"]}`"
107
 
108
  return {
109
  "profile_id": profile_id,
@@ -119,10 +141,10 @@ def update_profile_answers(answers_payload_str: str, request: gr.Request):
119
  Requires X-Auth-ID header for authentication.
120
  Answers_payload_str is expected to be a JSON string.
121
  """
122
- auth_id_header = request.headers.get("x-auth-id") # Headers are lowercased by Gradio/Starlette
123
- if not auth_id_header:
124
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
125
- print(f"update_profile_answers with Auth ID: {auth_id_header}")
126
 
127
  try:
128
  answers_payload = json.loads(answers_payload_str)
@@ -136,7 +158,7 @@ def update_profile_answers(answers_payload_str: str, request: gr.Request):
136
  target_profile_id = None
137
 
138
  for pid, profile_data in profiles.items():
139
- if profile_data.get("auth_id") == auth_id_header:
140
  user_profile = profile_data
141
  target_profile_id = pid
142
  break
@@ -186,21 +208,21 @@ def get_matches(request: gr.Request):
186
  Requires X-Auth-ID header for authentication.
187
  For P1, this returns up to 3 random profiles, excluding the user's own.
188
  """
189
- auth_id_header = request.headers.get("x-auth-id")
190
- if not auth_id_header:
191
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
192
- print(f"get_matches with Auth ID: {auth_id_header}")
193
 
194
  profiles = load_json_data(PROFILES_FILE, default_data={})
195
 
196
  requester_profile_id = None
197
  requester_profile = None
198
  for pid, profile_data in profiles.items():
199
- if profile_data.get("auth_id") == auth_id_header:
200
  requester_profile_id = pid
201
  requester_profile = profile_data
202
  break
203
-
204
  if not requester_profile:
205
  return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
206
 
@@ -273,7 +295,7 @@ def get_matches(request: gr.Request):
273
  "status": "success",
274
  "matches": matches_list,
275
  "instructions_for_agent": "You have received a list of matches. For each match, you can call `get_profile` using their `profile_id` to get more details. You can also use `send_message` to send a message to a match's `profile_id`. For each match, please format it in a nice card-like display (not raw JSON), including the match's name, gender, profile summary, and profile image (in a way that it should render in the chat client).",
276
- "instructions_for_user": "Here are some potential matches! Your AI agent can get more details on them ($0.10 per profile) or help you send a message ($1.00 per message). Or, if you don't like any of your matches, you can call `get_matches` again to get a new list."
277
  }
278
 
279
  def get_profile(profile_id_to_get: str, request: gr.Request):
@@ -281,18 +303,22 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
281
  Gets a user's full profile.
282
  Requires X-Auth-ID header for authentication.
283
  Access is free for viewing one's own profile.
284
- Accessing another user's profile has a cost (placeholder for P1).
285
  """
286
- auth_id_header = request.headers.get("x-auth-id")
287
- if not auth_id_header:
 
 
 
288
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
289
- print(f"get_profile with Auth ID: {auth_id_header}")
 
290
 
291
  profiles = load_json_data(PROFILES_FILE, default_data={})
292
 
293
  requester_profile_id = None
294
  for pid, profile_data in profiles.items():
295
- if profile_data.get("auth_id") == auth_id_header:
296
  requester_profile_id = pid
297
  break
298
 
@@ -304,6 +330,31 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
304
  if not target_profile:
305
  return {"status": "error", "message": f"Profile with ID '{profile_id_to_get}' not found."}
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  # For security, never return the auth_id
308
  target_profile.pop("auth_id", None)
309
 
@@ -317,16 +368,15 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
317
  target_profile['profile_image_url'] = image_url
318
  target_profile.pop("profile_image_filename", None)
319
 
 
320
  cost_incurred = 0.0
321
- instructions_for_agent = f"You have retrieved the profile for {profile_id_to_get}."
322
- instructions_for_user = f"Here is the profile for {profile_id_to_get}."
323
-
324
- if profile_id_to_get != requester_profile_id:
325
- # This is where a real payment would be processed.
326
- # For P1, we just note the cost.
327
- cost_incurred = 0.10
328
- instructions_for_agent += f" As this was not your own profile, a cost of ${cost_incurred:.2f} was incurred (for PoC, this is just a note)."
329
- instructions_for_user += " Viewing other profiles may have a cost."
330
 
331
  return {
332
  "status": "success",
@@ -338,20 +388,31 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
338
 
339
  def send_message(receiver_profile_id: str, content: str, request: gr.Request = None):
340
  """
341
- Sends a message to a match.
342
- Requires X-Auth-ID header for authentication.
343
- Costs $1.00 per message (placeholder for P1).
344
  """
345
- auth_id_header = request.headers.get("x-auth-id")
346
- if not auth_id_header:
 
 
 
347
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
348
- print(f"send_message with Auth ID: {auth_id_header}")
 
 
 
 
 
 
 
 
349
 
350
  profiles = load_json_data(PROFILES_FILE, default_data={})
351
 
352
  sender_profile_id = None
353
  for pid, profile_data in profiles.items():
354
- if profile_data.get("auth_id") == auth_id_header:
355
  sender_profile_id = pid
356
  break
357
 
@@ -361,9 +422,18 @@ def send_message(receiver_profile_id: str, content: str, request: gr.Request = N
361
  if receiver_profile_id not in profiles:
362
  return {"status": "error", "message": "Receiver profile ID not found."}
363
 
364
- # For P1, we are not integrating a real payment system.
365
- # We will integrate AgentPay here late.
366
- cost_incurred = 100
 
 
 
 
 
 
 
 
 
367
 
368
  messages = load_json_data(MESSAGES_FILE, default_data=[])
369
 
@@ -384,8 +454,8 @@ def send_message(receiver_profile_id: str, content: str, request: gr.Request = N
384
  return {
385
  "status": "success",
386
  "message_id": new_message["message_id"],
387
- "cost_incurred_usd_cents": cost_incurred,
388
- "instructions_for_agent": f"Message sent successfully to {receiver_profile_id}. A cost of {cost_incurred/100:.2f} cents was incurred (for PoC, this is just a note). You can get all messages for the user with `get_messages`.",
389
  "instructions_for_user": f"Your message has been sent to {receiver_profile_id}!"
390
  }
391
 
@@ -395,16 +465,16 @@ def get_messages(request: gr.Request):
395
  Marks retrieved messages where the user is the receiver as read for subsequent calls.
396
  Requires X-Auth-ID header for authentication.
397
  """
398
- auth_id_header = request.headers.get("x-auth-id")
399
- if not auth_id_header:
400
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
401
- print(f"get_messages with Auth ID: {auth_id_header}")
402
 
403
  profiles = load_json_data(PROFILES_FILE, default_data={})
404
 
405
  user_profile_id = None
406
  for pid, profile_data in profiles.items():
407
- if profile_data.get("auth_id") == auth_id_header:
408
  user_profile_id = pid
409
  break
410
 
@@ -451,8 +521,8 @@ def upload_profile_picture(image_upload, request: gr.Request):
451
  The new filename will be based on the user's profile_id.
452
  Requires X-Auth-ID header for authentication.
453
  """
454
- auth_id_header = request.headers.get("x-auth-id")
455
- if not auth_id_header:
456
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
457
 
458
  if image_upload is None:
@@ -463,7 +533,7 @@ def upload_profile_picture(image_upload, request: gr.Request):
463
  user_profile = None
464
  target_profile_id = None
465
  for pid, profile_data in profiles.items():
466
- if profile_data.get("auth_id") == auth_id_header:
467
  user_profile = profile_data
468
  target_profile_id = pid
469
  break
@@ -553,7 +623,7 @@ get_matches_demo = gr.Interface(
553
  inputs=None,
554
  outputs=gr.JSON(label="Matches"),
555
  title="Get Matches",
556
- description="Gets a list of potential matches for the authenticated user. Requires X-Auth-ID header."
557
  )
558
  # --- End of Get Matches Interface ---
559
 
@@ -563,7 +633,7 @@ get_profile_demo = gr.Interface(
563
  inputs=[gr.Textbox(label="Profile ID to Get")],
564
  outputs=gr.JSON(label="Profile Details"),
565
  title="Get Profile",
566
- description="Gets the full profile for a given Profile ID. Requires X-Auth-ID header."
567
  )
568
  # --- End of Get Profile Interface ---
569
 
@@ -576,7 +646,7 @@ send_message_demo = gr.Interface(
576
  ],
577
  outputs=gr.JSON(label="Send Status"),
578
  title="Send Message",
579
- description="Sends a message to another user. Requires X-Auth-ID header. ($1.00 placeholder cost)"
580
  )
581
  # --- End of Send Message Interface ---
582
 
 
5
  import os # Added for path joining
6
  import copy # For deep copying message list
7
  from datetime import datetime, timezone # Added timezone for UTC consistency
8
+ from dotenv import load_dotenv
9
+
10
+ # AgentPay SDK imports
11
+ from agentpay_sdk import AgentPayClient
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ # --- AgentPay Configuration ---
17
+
18
+ try:
19
+ agentpay_client = AgentPayClient(service_token=os.getenv("AGENTPAY_SERVICE_TOKEN", ""))
20
+ print("AgentPay client initialized successfully")
21
+ except Exception as e:
22
+ print(f"Warning: AgentPay client initialization failed: {e}")
23
+ agentpay_client = None
24
+
25
+ # AgentPay pricing in cents
26
+ PRICING = {
27
+ "send_message": 100, # $1.00 = 100 cents
28
+ "get_profile": 10 # $0.10 = 10 cents
29
+ }
30
 
31
  # --- Start of JSON I/O Helper Functions ---
32
 
 
124
  # Decide how to handle this error - maybe return an error to the client?
125
 
126
  # 4. Return IDs, questionnaire data, and instructions
127
+ instructions_for_agent = "You have received a `profile_id` (public identifier for this user\'s profile) and an `auth_id` (private key for authentication for this user). Please ask the user to store both securely. The `auth_id` must be sent as an `X-Auth-ID` header in subsequent requests that require authentication for this user (e.g., `update_profile_answers`, `get_matches`, `get_messages`, `get_profile` for own profile, `send_message`). The `profile_id` is used to publicly identify this user to others (e.g., in matches, or when sending/receiving messages). Before you proceed, please ask the user to update their MCP configuration to include their new Auth ID by adding the following to their mcp.json (or similar) configuration file: `\"matchmaker\": {\"command\": \"npx\", \"args\": [\"mcp-remote\", \"http://localhost:7860/gradio_api/mcp/sse\", \"--allow-http\", \"--header\", \"X-AUTH-ID:<your-auth-id>\"]}`, then to RESTART THE CLIENT (especially if you are Claude), or to try RECONNECTING (e.g. if you are Cursor or otherwise). Once they have done so, you must then walk the user through the questionnaire, preferably question by question, and collect the answers (in preparation to update the user's profile using update_profile_answers). If the user provides answers that are not in the list of valid answers, please ask them to provide a valid answer."
128
+ instructions_for_user = "Your profile creation process has started! You\'ve been assigned a unique Profile ID and a secret Auth ID. Your AI agent will use these to manage your profile and interactions. Please walk through the questionnaire with your Agent, and provide your answers. You can upload a profile picture later."
129
 
130
  return {
131
  "profile_id": profile_id,
 
141
  Requires X-Auth-ID header for authentication.
142
  Answers_payload_str is expected to be a JSON string.
143
  """
144
+ auth_id = request.headers.get("x-auth-id") # Headers are lowercased by Gradio/Starlette
145
+ if not auth_id:
146
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
147
+ print(f"update_profile_answers with Auth ID: {auth_id}")
148
 
149
  try:
150
  answers_payload = json.loads(answers_payload_str)
 
158
  target_profile_id = None
159
 
160
  for pid, profile_data in profiles.items():
161
+ if profile_data.get("auth_id") == auth_id:
162
  user_profile = profile_data
163
  target_profile_id = pid
164
  break
 
208
  Requires X-Auth-ID header for authentication.
209
  For P1, this returns up to 3 random profiles, excluding the user's own.
210
  """
211
+ auth_id = request.headers.get("x-auth-id")
212
+ if not auth_id:
213
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
214
+ print(f"get_matches with Auth ID: {auth_id}")
215
 
216
  profiles = load_json_data(PROFILES_FILE, default_data={})
217
 
218
  requester_profile_id = None
219
  requester_profile = None
220
  for pid, profile_data in profiles.items():
221
+ if profile_data.get("auth_id") == auth_id:
222
  requester_profile_id = pid
223
  requester_profile = profile_data
224
  break
225
+
226
  if not requester_profile:
227
  return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
228
 
 
295
  "status": "success",
296
  "matches": matches_list,
297
  "instructions_for_agent": "You have received a list of matches. For each match, you can call `get_profile` using their `profile_id` to get more details. You can also use `send_message` to send a message to a match's `profile_id`. For each match, please format it in a nice card-like display (not raw JSON), including the match's name, gender, profile summary, and profile image (in a way that it should render in the chat client).",
298
+ "instructions_for_user": "Here are some potential matches! Your AI agent can get more details on them ($0.10 per profile) or help you send a message ($1.00 per message). Or, if you don't like any of your matches, you can call `get_matches` again to get a new list (free)."
299
  }
300
 
301
  def get_profile(profile_id_to_get: str, request: gr.Request):
 
303
  Gets a user's full profile.
304
  Requires X-Auth-ID header for authentication.
305
  Access is free for viewing one's own profile.
306
+ Accessing another user's profile costs $0.10 and requires X-AGENTPAY-API-KEY.
307
  """
308
+ # Extract headers directly
309
+ auth_id = request.headers.get("x-auth-id")
310
+ agentpay_api_key = request.headers.get("x-agentpay-api-key")
311
+
312
+ if not auth_id:
313
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
314
+
315
+ print(f"get_profile with Auth ID: {auth_id}")
316
 
317
  profiles = load_json_data(PROFILES_FILE, default_data={})
318
 
319
  requester_profile_id = None
320
  for pid, profile_data in profiles.items():
321
+ if profile_data.get("auth_id") == auth_id:
322
  requester_profile_id = pid
323
  break
324
 
 
330
  if not target_profile:
331
  return {"status": "error", "message": f"Profile with ID '{profile_id_to_get}' not found."}
332
 
333
+ # Check if viewing another user's profile (requires payment)
334
+ is_viewing_other = profile_id_to_get != requester_profile_id
335
+
336
+ if is_viewing_other:
337
+ # Require AgentPay API key for viewing other profiles
338
+ if not agentpay_api_key:
339
+ return {"status": "error", "message": "Payment failed: X-AGENTPAY-API-KEY header is required to view other profiles."}
340
+
341
+ # Validate AgentPay API key directly
342
+ if not agentpay_client or not agentpay_client.validate_api_key(agentpay_api_key):
343
+ return {"status": "error", "message": "Payment failed: Invalid AgentPay API key"}
344
+
345
+ # Charge for viewing other profile via AgentPay
346
+ usage_id = str(uuid.uuid4())
347
+ amount_cents = PRICING["get_profile"]
348
+ result = agentpay_client.consume(
349
+ api_key=agentpay_api_key,
350
+ amount_cents=amount_cents,
351
+ usage_event_id=usage_id,
352
+ metadata={"tool": "get_profile", "requester": requester_profile_id, "target": profile_id_to_get}
353
+ )
354
+
355
+ if not result.success:
356
+ return {"status": "error", "message": f"Payment failed: {result.error_message}"}
357
+
358
  # For security, never return the auth_id
359
  target_profile.pop("auth_id", None)
360
 
 
368
  target_profile['profile_image_url'] = image_url
369
  target_profile.pop("profile_image_filename", None)
370
 
371
+ # Update cost and messaging based on whether payment was charged
372
  cost_incurred = 0.0
373
+ if is_viewing_other:
374
+ cost_incurred = PRICING["get_profile"] / 100.0 # Convert cents to dollars
375
+ instructions_for_agent = f"You have retrieved the profile for {profile_id_to_get}. A cost of {cost_incurred} was charged via AgentPay."
376
+ instructions_for_user = f"Here is the profile for {profile_id_to_get}."
377
+ else:
378
+ instructions_for_agent = f"You have retrieved your own profile ({profile_id_to_get}). No cost was incurred."
379
+ instructions_for_user = f"Here is your profile."
 
 
380
 
381
  return {
382
  "status": "success",
 
388
 
389
  def send_message(receiver_profile_id: str, content: str, request: gr.Request = None):
390
  """
391
+ Sends a message to a match.
392
+ Requires X-Auth-ID header for authentication and X-AGENTPAY-API-KEY for payment.
393
+ Costs $1.00 per message.
394
  """
395
+ # Extract headers directly
396
+ auth_id = request.headers.get("x-auth-id")
397
+ agentpay_api_key = request.headers.get("x-agentpay-api-key")
398
+
399
+ if not auth_id:
400
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
401
+
402
+ if not agentpay_api_key:
403
+ return {"status": "error", "message": "Payment failed: X-AGENTPAY-API-KEY header is missing."}
404
+
405
+ # Validate AgentPay API key directly
406
+ if not agentpay_client or not agentpay_client.validate_api_key(agentpay_api_key):
407
+ return {"status": "error", "message": "Payment failed: Invalid AgentPay API key"}
408
+
409
+ print(f"send_message with Auth ID: {auth_id}")
410
 
411
  profiles = load_json_data(PROFILES_FILE, default_data={})
412
 
413
  sender_profile_id = None
414
  for pid, profile_data in profiles.items():
415
+ if profile_data.get("auth_id") == auth_id:
416
  sender_profile_id = pid
417
  break
418
 
 
422
  if receiver_profile_id not in profiles:
423
  return {"status": "error", "message": "Receiver profile ID not found."}
424
 
425
+ # Charge for sending message via AgentPay
426
+ usage_id = str(uuid.uuid4())
427
+ amount_cents = PRICING["send_message"]
428
+ result = agentpay_client.consume(
429
+ api_key=agentpay_api_key,
430
+ amount_cents=amount_cents,
431
+ usage_event_id=usage_id,
432
+ metadata={"tool": "send_message", "sender": sender_profile_id, "receiver": receiver_profile_id}
433
+ )
434
+
435
+ if not result.success:
436
+ return {"status": "error", "message": f"Payment failed: {result.error_message}"}
437
 
438
  messages = load_json_data(MESSAGES_FILE, default_data=[])
439
 
 
454
  return {
455
  "status": "success",
456
  "message_id": new_message["message_id"],
457
+ "cost_incurred_usd_cents": amount_cents,
458
+ "instructions_for_agent": f"Message sent successfully to {receiver_profile_id}. A cost of ${amount_cents/100:.2f} was charged via AgentPay. You can get all messages for the user with `get_messages`.",
459
  "instructions_for_user": f"Your message has been sent to {receiver_profile_id}!"
460
  }
461
 
 
465
  Marks retrieved messages where the user is the receiver as read for subsequent calls.
466
  Requires X-Auth-ID header for authentication.
467
  """
468
+ auth_id = request.headers.get("x-auth-id")
469
+ if not auth_id:
470
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
471
+ print(f"get_messages with Auth ID: {auth_id}")
472
 
473
  profiles = load_json_data(PROFILES_FILE, default_data={})
474
 
475
  user_profile_id = None
476
  for pid, profile_data in profiles.items():
477
+ if profile_data.get("auth_id") == auth_id:
478
  user_profile_id = pid
479
  break
480
 
 
521
  The new filename will be based on the user's profile_id.
522
  Requires X-Auth-ID header for authentication.
523
  """
524
+ auth_id = request.headers.get("x-auth-id")
525
+ if not auth_id:
526
  return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
527
 
528
  if image_upload is None:
 
533
  user_profile = None
534
  target_profile_id = None
535
  for pid, profile_data in profiles.items():
536
+ if profile_data.get("auth_id") == auth_id:
537
  user_profile = profile_data
538
  target_profile_id = pid
539
  break
 
623
  inputs=None,
624
  outputs=gr.JSON(label="Matches"),
625
  title="Get Matches",
626
+ description="Gets a list of potential matches for the authenticated user. Requires X-Auth-ID header. FREE service."
627
  )
628
  # --- End of Get Matches Interface ---
629
 
 
633
  inputs=[gr.Textbox(label="Profile ID to Get")],
634
  outputs=gr.JSON(label="Profile Details"),
635
  title="Get Profile",
636
+ description="Gets the full profile for a given Profile ID. Requires X-Auth-ID header. Free for own profile, $0.10 for others (requires X-AGENTPAY-API-KEY)."
637
  )
638
  # --- End of Get Profile Interface ---
639
 
 
646
  ],
647
  outputs=gr.JSON(label="Send Status"),
648
  title="Send Message",
649
+ description="Sends a message to another user. Requires X-Auth-ID and X-AGENTPAY-API-KEY headers. ($1.00 per message)"
650
  )
651
  # --- End of Send Message Interface ---
652
 
data/messages.json CHANGED
@@ -63,5 +63,29 @@
63
  "content": "Hey there",
64
  "timestamp": "2025-06-06T09:28:13.285402+00:00",
65
  "read_status": false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
  ]
 
63
  "content": "Hey there",
64
  "timestamp": "2025-06-06T09:28:13.285402+00:00",
65
  "read_status": false
66
+ },
67
+ {
68
+ "message_id": "044124be-3081-4ce4-a4fa-a80245dafc2f",
69
+ "sender_profile_id": "user_e6chs11v",
70
+ "receiver_profile_id": "user_d8x3v7n1",
71
+ "content": "Hi-acynth",
72
+ "timestamp": "2025-06-09T08:46:21.258372+00:00",
73
+ "read_status": false
74
+ },
75
+ {
76
+ "message_id": "62f5d6ac-33f5-4f86-93f1-5d84b9fe73f9",
77
+ "sender_profile_id": "user_5ru2ht5q",
78
+ "receiver_profile_id": "user_l9z4f2x7",
79
+ "content": "Hi Maya! I'm John - I saw you're a data scientist who loves cooking Italian food. As someone who appreciates things being clean and organized (I'm a janitor!), I have a lot of respect for both data visualization and a good homemade pasta recipe. I'm more of a homebody, but I'd love to hear about your rock climbing adventures over a cozy podcast listening session. Maybe you could teach me to make that famous pasta of yours?",
80
+ "timestamp": "2025-06-10T15:54:56.209149+00:00",
81
+ "read_status": false
82
+ },
83
+ {
84
+ "message_id": "2bc9a582-e677-45e3-91bc-cd63472f4922",
85
+ "sender_profile_id": "user_5ru2ht5q",
86
+ "receiver_profile_id": "user_m4k7r9n2",
87
+ "content": "Hi Emma! I'm John - I really admire your balanced approach to life with yoga and farmers markets. As someone who takes pride in keeping things clean and organized (I'm a janitor), I have a deep appreciation for the quality and freshness you find at farmers markets. I'm more of a homebody, but I'd love to have some genuine conversations about your travels and maybe learn about your favorite market finds. Your optimistic vibe sounds like it would bring great energy to cozy evenings in!",
88
+ "timestamp": "2025-06-10T16:12:01.428426+00:00",
89
+ "read_status": false
90
  }
91
  ]
data/profiles.json CHANGED
@@ -280,5 +280,37 @@
280
  "q_vibe": "Caring and generous.",
281
  "q_gender_preference": "Men"
282
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
  }
 
280
  "q_vibe": "Caring and generous.",
281
  "q_gender_preference": "Men"
282
  }
283
+ },
284
+ "user_e6chs11v": {
285
+ "profile_id": "user_e6chs11v",
286
+ "auth_id": "b9b8e91f-8369-4eff-a804-5a28a83f3cf3",
287
+ "created_at": "2025-06-09T08:32:50.340435+00:00",
288
+ "updated_at": "2025-06-09T08:38:05.614570+00:00",
289
+ "name": "George",
290
+ "gender": "Man",
291
+ "profile_summary": "I'm George the great",
292
+ "profile_image_filename": "default.jpg",
293
+ "answers": {
294
+ "q_gender_preference": "Women",
295
+ "q_hobby": "Gardening",
296
+ "q_looking_for": "I'm looking for someone to grow my love with.",
297
+ "q_vibe": "Homebody"
298
+ }
299
+ },
300
+ "user_5ru2ht5q": {
301
+ "profile_id": "user_5ru2ht5q",
302
+ "auth_id": "e98bbb1e-0359-489b-b552-554a6eb22b21",
303
+ "created_at": "2025-06-10T15:45:43.284107+00:00",
304
+ "updated_at": "2025-06-10T15:48:45.874926+00:00",
305
+ "name": "John",
306
+ "gender": "Man",
307
+ "profile_summary": "I'm John, the janitor.",
308
+ "profile_image_filename": "default.jpg",
309
+ "answers": {
310
+ "q_gender_preference": "Women",
311
+ "q_hobby": "Cleaning things.",
312
+ "q_looking_for": "Someone neat and tidy to spend time with.",
313
+ "q_vibe": "Homebody"
314
+ }
315
  }
316
  }
requirements.txt CHANGED
@@ -1 +1,3 @@
1
  gradio[mcp]
 
 
 
1
  gradio[mcp]
2
+ requests
3
+ python-dotenv