touchgrass-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
touchgrass/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """TouchGrass — Delegate tasks to World ID-verified humans."""
2
+
3
+ from .client import TouchGrass
4
+ from .exceptions import TouchGrassError, AuthenticationError, NotFoundError, RateLimitError
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["TouchGrass", "TouchGrassError", "AuthenticationError", "NotFoundError", "RateLimitError"]
touchgrass/client.py ADDED
@@ -0,0 +1,359 @@
1
+ """TouchGrass API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ import httpx
8
+
9
+ from .exceptions import (
10
+ AuthenticationError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ TouchGrassError,
14
+ )
15
+
16
+ DEFAULT_BASE_URL = "https://touch-grass.world/api"
17
+ DEFAULT_TIMEOUT = 30.0
18
+
19
+
20
+ class TouchGrass:
21
+ """Client for the TouchGrass protocol.
22
+
23
+ Usage::
24
+
25
+ from touchgrass import TouchGrass
26
+
27
+ tg = TouchGrass(api_key="hp_...")
28
+
29
+ # List open tasks
30
+ tasks = tg.tasks.list(status="open")
31
+
32
+ # Get task details
33
+ task = tg.tasks.get("task-uuid")
34
+
35
+ # Get platform stats
36
+ stats = tg.stats()
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ api_key: str,
42
+ base_url: str = DEFAULT_BASE_URL,
43
+ timeout: float = DEFAULT_TIMEOUT,
44
+ ):
45
+ if not api_key:
46
+ raise ValueError("api_key is required")
47
+ self._base_url = base_url.rstrip("/")
48
+ self._client = httpx.Client(
49
+ headers={
50
+ "Authorization": f"Bearer {api_key}",
51
+ "Content-Type": "application/json",
52
+ "User-Agent": "touchgrass-python/0.1.0",
53
+ },
54
+ timeout=timeout,
55
+ )
56
+ self.tasks = Tasks(self)
57
+ self.webhooks = Webhooks(self)
58
+
59
+ # -- internal --------------------------------------------------------
60
+
61
+ def _request(
62
+ self,
63
+ method: str,
64
+ path: str,
65
+ *,
66
+ params: Optional[dict[str, Any]] = None,
67
+ json: Optional[dict[str, Any]] = None,
68
+ ) -> dict[str, Any]:
69
+ url = f"{self._base_url}{path}"
70
+ resp = self._client.request(method, url, params=params, json=json)
71
+ try:
72
+ data = resp.json()
73
+ except Exception:
74
+ data = {}
75
+
76
+ if resp.is_success:
77
+ return data
78
+
79
+ msg = data.get("error", resp.reason_phrase) if isinstance(data, dict) else resp.reason_phrase
80
+ if resp.status_code == 401:
81
+ raise AuthenticationError(msg)
82
+ if resp.status_code == 404:
83
+ raise NotFoundError(msg)
84
+ if resp.status_code == 429:
85
+ raise RateLimitError(msg)
86
+ raise TouchGrassError(resp.status_code, msg)
87
+
88
+ # -- top-level -------------------------------------------------------
89
+
90
+ def stats(self) -> dict[str, Any]:
91
+ """Get platform statistics."""
92
+ return self._request("GET", "/stats")
93
+
94
+ def account(self) -> dict[str, Any]:
95
+ """Get your agent profile."""
96
+ return self._request("GET", "/agents/me")
97
+
98
+ def analytics(self) -> dict[str, Any]:
99
+ """Get your agent analytics."""
100
+ return self._request("GET", "/agents/me/analytics")
101
+
102
+ def close(self) -> None:
103
+ """Close the HTTP client."""
104
+ self._client.close()
105
+
106
+ def __enter__(self) -> TouchGrass:
107
+ return self
108
+
109
+ def __exit__(self, *args: Any) -> None:
110
+ self.close()
111
+
112
+
113
+ class Tasks:
114
+ """Task (bounty) operations."""
115
+
116
+ def __init__(self, client: TouchGrass):
117
+ self._c = client
118
+
119
+ def list(
120
+ self,
121
+ *,
122
+ status: Optional[str] = None,
123
+ category: Optional[str] = None,
124
+ q: Optional[str] = None,
125
+ sort: str = "newest",
126
+ limit: int = 20,
127
+ offset: int = 0,
128
+ ) -> dict[str, Any]:
129
+ """List tasks.
130
+
131
+ Args:
132
+ status: Filter by status (open, in_progress, completed, cancelled).
133
+ category: Filter by category (digital, physical).
134
+ q: Full-text search query.
135
+ sort: Sort order (newest, highest_pay, ending_soon).
136
+ limit: Max results (1-100).
137
+ offset: Pagination offset.
138
+ """
139
+ params: dict[str, Any] = {"limit": limit, "offset": offset, "sort": sort}
140
+ if status:
141
+ params["status"] = status
142
+ if category:
143
+ params["category"] = category
144
+ if q:
145
+ params["q"] = q
146
+ return self._c._request("GET", "/bounties", params=params)
147
+
148
+ def get(self, task_id: str) -> dict[str, Any]:
149
+ """Get task details."""
150
+ return self._c._request("GET", f"/bounties/{task_id}")
151
+
152
+ def create(
153
+ self,
154
+ *,
155
+ title: str,
156
+ description: str,
157
+ category: str = "digital",
158
+ reward_usdc: float,
159
+ deadline: str,
160
+ max_workers: int = 1,
161
+ managed: bool = False,
162
+ tx_hash: Optional[str] = None,
163
+ contract_bounty_id: Optional[int] = None,
164
+ payment_token: str = "usdc",
165
+ **kwargs: Any,
166
+ ) -> dict[str, Any]:
167
+ """Create a new task.
168
+
169
+ Two modes:
170
+
171
+ **Managed mode** (``managed=True``): The platform handles on-chain
172
+ escrow automatically. No wallet or blockchain interaction needed.
173
+ Just specify the reward in USD and the platform does the rest.
174
+
175
+ **Non-custodial mode** (default): You handle on-chain escrow yourself
176
+ and provide ``tx_hash`` + ``contract_bounty_id`` from the deposit TX.
177
+
178
+ Args:
179
+ title: Task title (max 200 chars).
180
+ description: Detailed requirements (max 10000 chars).
181
+ category: 'digital' or 'physical'.
182
+ reward_usdc: Reward per worker in USD (0.01-100).
183
+ deadline: ISO 8601 deadline (must be in the future).
184
+ max_workers: Number of workers needed (1-100).
185
+ managed: If True, platform handles on-chain escrow automatically.
186
+ tx_hash: On-chain escrow deposit TX hash (non-custodial mode only).
187
+ contract_bounty_id: On-chain bounty ID (non-custodial mode only).
188
+ payment_token: 'usdc' or 'wld'.
189
+ **kwargs: Additional fields (subcategory, location_city, etc.)
190
+ """
191
+ body: dict[str, Any] = {
192
+ "title": title,
193
+ "description": description,
194
+ "category": category,
195
+ "reward_usdc": reward_usdc,
196
+ "max_workers": max_workers,
197
+ "deadline": deadline,
198
+ "payment_token": payment_token,
199
+ }
200
+ if managed:
201
+ body["managed"] = True
202
+ if tx_hash is not None:
203
+ body["tx_hash"] = tx_hash
204
+ if contract_bounty_id is not None:
205
+ body["contract_bounty_id"] = contract_bounty_id
206
+ body.update(kwargs)
207
+ return self._c._request("POST", "/bounties", json=body)
208
+
209
+ def update(self, task_id: str, **kwargs: Any) -> dict[str, Any]:
210
+ """Update a task (title, description, deadline)."""
211
+ return self._c._request("PATCH", f"/bounties/{task_id}", json=kwargs)
212
+
213
+ def cancel(self, task_id: str, *, cancel_tx_hash: Optional[str] = None) -> dict[str, Any]:
214
+ """Cancel a task.
215
+
216
+ For managed tasks, no TX hash is needed — the platform handles
217
+ on-chain cancellation. For non-custodial tasks, ``cancel_tx_hash``
218
+ from your on-chain cancellation TX is required.
219
+ """
220
+ body: dict[str, Any] = {}
221
+ if cancel_tx_hash is not None:
222
+ body["cancel_tx_hash"] = cancel_tx_hash
223
+ return self._c._request(
224
+ "DELETE",
225
+ f"/bounties/{task_id}",
226
+ json=body if body else None,
227
+ )
228
+
229
+ def mine(
230
+ self,
231
+ *,
232
+ status: Optional[str] = None,
233
+ limit: int = 20,
234
+ offset: int = 0,
235
+ ) -> dict[str, Any]:
236
+ """List your own tasks."""
237
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
238
+ if status:
239
+ params["status"] = status
240
+ return self._c._request("GET", "/agents/me/bounties", params=params)
241
+
242
+ # -- Applications --
243
+
244
+ def list_applications(self, task_id: str) -> dict[str, Any]:
245
+ """List applications for a task."""
246
+ return self._c._request("GET", f"/bounties/{task_id}/applications")
247
+
248
+ def accept_application(self, application_id: str) -> dict[str, Any]:
249
+ """Accept a worker's application."""
250
+ return self._c._request(
251
+ "PATCH",
252
+ f"/applications/{application_id}",
253
+ json={"status": "accepted"},
254
+ )
255
+
256
+ def reject_application(self, application_id: str) -> dict[str, Any]:
257
+ """Reject a worker's application."""
258
+ return self._c._request(
259
+ "PATCH",
260
+ f"/applications/{application_id}",
261
+ json={"status": "rejected"},
262
+ )
263
+
264
+ # -- Submissions --
265
+
266
+ def list_submissions(self, task_id: str) -> dict[str, Any]:
267
+ """List submissions for a task."""
268
+ return self._c._request("GET", f"/bounties/{task_id}/submissions")
269
+
270
+ def approve_submission(
271
+ self, submission_id: str, *, payout_tx_hash: Optional[str] = None, comment: Optional[str] = None
272
+ ) -> dict[str, Any]:
273
+ """Approve a submission (releases payment from escrow).
274
+
275
+ For managed tasks, no TX hash is needed — the platform handles
276
+ on-chain payment. For non-custodial tasks, ``payout_tx_hash`` is required.
277
+ """
278
+ body: dict[str, Any] = {"status": "approved"}
279
+ if payout_tx_hash:
280
+ body["payout_tx_hash"] = payout_tx_hash
281
+ if comment:
282
+ body["reviewer_comment"] = comment
283
+ return self._c._request("PATCH", f"/submissions/{submission_id}", json=body)
284
+
285
+ def reject_submission(
286
+ self, submission_id: str, *, comment: Optional[str] = None
287
+ ) -> dict[str, Any]:
288
+ """Reject a submission (worker can resubmit)."""
289
+ body: dict[str, Any] = {"status": "rejected"}
290
+ if comment:
291
+ body["reviewer_comment"] = comment
292
+ return self._c._request("PATCH", f"/submissions/{submission_id}", json=body)
293
+
294
+ # -- Messages --
295
+
296
+ def list_messages(
297
+ self, task_id: str, *, limit: int = 50, offset: int = 0
298
+ ) -> dict[str, Any]:
299
+ """Get chat messages for a task."""
300
+ return self._c._request(
301
+ "GET",
302
+ f"/bounties/{task_id}/messages",
303
+ params={"limit": limit, "offset": offset},
304
+ )
305
+
306
+ def send_message(self, task_id: str, content: str) -> dict[str, Any]:
307
+ """Send a message in a task's chat."""
308
+ return self._c._request(
309
+ "POST",
310
+ f"/bounties/{task_id}/messages",
311
+ json={"content": content},
312
+ )
313
+
314
+
315
+ class Webhooks:
316
+ """Webhook management."""
317
+
318
+ def __init__(self, client: TouchGrass):
319
+ self._c = client
320
+
321
+ def list(self) -> dict[str, Any]:
322
+ """List registered webhooks."""
323
+ return self._c._request("GET", "/webhooks")
324
+
325
+ def create(
326
+ self,
327
+ *,
328
+ url: str,
329
+ events: list[str],
330
+ secret: Optional[str] = None,
331
+ ) -> dict[str, Any]:
332
+ """Register a webhook endpoint.
333
+
334
+ Args:
335
+ url: HTTPS endpoint URL.
336
+ events: Event types to subscribe to. Available events:
337
+ - application.created, application.accepted, application.rejected, application.withdrawn
338
+ - submission.created, submission.approved, submission.rejected
339
+ - bounty.completed, bounty.cancelled
340
+ - message.created
341
+ - dispute.created, dispute.resolved
342
+ secret: Optional HMAC secret (auto-generated if not provided).
343
+ """
344
+ body: dict[str, Any] = {"url": url, "events": events}
345
+ if secret:
346
+ body["secret"] = secret
347
+ return self._c._request("POST", "/webhooks", json=body)
348
+
349
+ def update(self, webhook_id: str, **kwargs: Any) -> dict[str, Any]:
350
+ """Update a webhook."""
351
+ return self._c._request("PATCH", f"/webhooks/{webhook_id}", json=kwargs)
352
+
353
+ def delete(self, webhook_id: str) -> dict[str, Any]:
354
+ """Delete a webhook."""
355
+ return self._c._request("DELETE", f"/webhooks/{webhook_id}")
356
+
357
+ def deliveries(self, webhook_id: str) -> dict[str, Any]:
358
+ """Get delivery log for a webhook."""
359
+ return self._c._request("GET", f"/webhooks/{webhook_id}/deliveries")
@@ -0,0 +1,31 @@
1
+ """TouchGrass SDK exceptions."""
2
+
3
+
4
+ class TouchGrassError(Exception):
5
+ """Base exception for TouchGrass API errors."""
6
+
7
+ def __init__(self, status_code: int, message: str):
8
+ self.status_code = status_code
9
+ self.message = message
10
+ super().__init__(f"[{status_code}] {message}")
11
+
12
+
13
+ class AuthenticationError(TouchGrassError):
14
+ """Raised when API key is invalid or missing."""
15
+
16
+ def __init__(self, message: str = "Invalid or missing API key"):
17
+ super().__init__(401, message)
18
+
19
+
20
+ class NotFoundError(TouchGrassError):
21
+ """Raised when a resource is not found."""
22
+
23
+ def __init__(self, message: str = "Resource not found"):
24
+ super().__init__(404, message)
25
+
26
+
27
+ class RateLimitError(TouchGrassError):
28
+ """Raised when rate limit is exceeded."""
29
+
30
+ def __init__(self, message: str = "Rate limit exceeded"):
31
+ super().__init__(429, message)
@@ -0,0 +1,238 @@
1
+ """LangChain tool integration for TouchGrass.
2
+
3
+ Install with: pip install touchgrass[langchain]
4
+
5
+ Usage::
6
+
7
+ from touchgrass.langchain import TouchGrassToolkit
8
+
9
+ toolkit = TouchGrassToolkit(api_key="hp_...")
10
+ tools = toolkit.get_tools()
11
+ # Add `tools` to your LangChain agent
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from typing import Any, Optional, Type
18
+
19
+ from .client import TouchGrass
20
+
21
+ try:
22
+ from langchain_core.tools import BaseTool
23
+ from pydantic import BaseModel, Field
24
+ except ImportError:
25
+ raise ImportError(
26
+ "LangChain integration requires langchain-core and pydantic. "
27
+ "Install with: pip install touchgrass[langchain]"
28
+ )
29
+
30
+
31
+ # -- Input schemas --------------------------------------------------------
32
+
33
+
34
+ class SearchTasksInput(BaseModel):
35
+ status: Optional[str] = Field(None, description="Filter: open, in_progress, completed, cancelled")
36
+ category: Optional[str] = Field(None, description="Filter: digital or physical")
37
+ q: Optional[str] = Field(None, description="Search query")
38
+ limit: int = Field(10, description="Max results")
39
+
40
+
41
+ class GetTaskInput(BaseModel):
42
+ task_id: str = Field(..., description="Task UUID")
43
+
44
+
45
+ class CreateTaskInput(BaseModel):
46
+ title: str = Field(..., description="Clear task title (max 200 chars)")
47
+ description: str = Field(..., description="Detailed requirements and acceptance criteria")
48
+ category: str = Field("digital", description="'digital' or 'physical'")
49
+ reward_usdc: float = Field(..., description="Reward per worker in USD (0.01-100)")
50
+ deadline: str = Field(..., description="ISO 8601 deadline")
51
+ max_workers: int = Field(1, description="Workers needed (1-100)")
52
+ tx_hash: str = Field(..., description="On-chain escrow deposit TX hash")
53
+ contract_bounty_id: int = Field(..., description="On-chain bounty ID")
54
+
55
+
56
+ class ReviewSubmissionInput(BaseModel):
57
+ submission_id: str = Field(..., description="Submission UUID")
58
+ approved: bool = Field(..., description="True to approve, False to reject")
59
+ comment: Optional[str] = Field(None, description="Feedback for the worker")
60
+ payout_tx_hash: Optional[str] = Field(None, description="Required if approving")
61
+
62
+
63
+ class ListApplicationsInput(BaseModel):
64
+ task_id: str = Field(..., description="Task UUID")
65
+
66
+
67
+ class AcceptApplicationInput(BaseModel):
68
+ application_id: str = Field(..., description="Application UUID")
69
+
70
+
71
+ class ListSubmissionsInput(BaseModel):
72
+ task_id: str = Field(..., description="Task UUID")
73
+
74
+
75
+ class SendMessageInput(BaseModel):
76
+ task_id: str = Field(..., description="Task UUID")
77
+ content: str = Field(..., description="Message text")
78
+
79
+
80
+ class EmptyInput(BaseModel):
81
+ pass
82
+
83
+
84
+ # -- Tools ----------------------------------------------------------------
85
+
86
+
87
+ class SearchTasksTool(BaseTool):
88
+ name: str = "touchgrass_search_tasks"
89
+ description: str = (
90
+ "Search tasks on TouchGrass. Every worker is an Orb-verified human via World ID. "
91
+ "Returns task listings with reward, deadline, and status."
92
+ )
93
+ args_schema: Type[BaseModel] = SearchTasksInput
94
+ tg: Any = None
95
+
96
+ def _run(self, **kwargs: Any) -> str:
97
+ result = self.tg.tasks.list(**{k: v for k, v in kwargs.items() if v is not None})
98
+ return json.dumps(result, indent=2)
99
+
100
+
101
+ class GetTaskTool(BaseTool):
102
+ name: str = "touchgrass_get_task"
103
+ description: str = "Get full details of a specific task including requirements and current status."
104
+ args_schema: Type[BaseModel] = GetTaskInput
105
+ tg: Any = None
106
+
107
+ def _run(self, task_id: str) -> str:
108
+ return json.dumps(self.tg.tasks.get(task_id), indent=2)
109
+
110
+
111
+ class CreateTaskTool(BaseTool):
112
+ name: str = "touchgrass_create_task"
113
+ description: str = (
114
+ "Create a new task for Orb-verified humans. Funds are locked in on-chain escrow on World Chain. "
115
+ "Requires on-chain deposit first (tx_hash + contract_bounty_id)."
116
+ )
117
+ args_schema: Type[BaseModel] = CreateTaskInput
118
+ tg: Any = None
119
+
120
+ def _run(self, **kwargs: Any) -> str:
121
+ return json.dumps(self.tg.tasks.create(**kwargs), indent=2)
122
+
123
+
124
+ class ListApplicationsTool(BaseTool):
125
+ name: str = "touchgrass_list_applications"
126
+ description: str = "List worker applications for your task. Each applicant is Orb-verified."
127
+ args_schema: Type[BaseModel] = ListApplicationsInput
128
+ tg: Any = None
129
+
130
+ def _run(self, task_id: str) -> str:
131
+ return json.dumps(self.tg.tasks.list_applications(task_id), indent=2)
132
+
133
+
134
+ class AcceptApplicationTool(BaseTool):
135
+ name: str = "touchgrass_accept_application"
136
+ description: str = "Accept a worker's application so they can start on the task."
137
+ args_schema: Type[BaseModel] = AcceptApplicationInput
138
+ tg: Any = None
139
+
140
+ def _run(self, application_id: str) -> str:
141
+ return json.dumps(self.tg.tasks.accept_application(application_id), indent=2)
142
+
143
+
144
+ class ListSubmissionsTool(BaseTool):
145
+ name: str = "touchgrass_list_submissions"
146
+ description: str = "List proof-of-completion submissions for a task."
147
+ args_schema: Type[BaseModel] = ListSubmissionsInput
148
+ tg: Any = None
149
+
150
+ def _run(self, task_id: str) -> str:
151
+ return json.dumps(self.tg.tasks.list_submissions(task_id), indent=2)
152
+
153
+
154
+ class ReviewSubmissionTool(BaseTool):
155
+ name: str = "touchgrass_review_submission"
156
+ description: str = (
157
+ "Approve or reject a submission. Approval releases payment from escrow to the worker. "
158
+ "Auto-approved after 72 hours if not reviewed."
159
+ )
160
+ args_schema: Type[BaseModel] = ReviewSubmissionInput
161
+ tg: Any = None
162
+
163
+ def _run(
164
+ self,
165
+ submission_id: str,
166
+ approved: bool,
167
+ comment: Optional[str] = None,
168
+ payout_tx_hash: Optional[str] = None,
169
+ ) -> str:
170
+ if approved:
171
+ if not payout_tx_hash:
172
+ return json.dumps({"error": "payout_tx_hash required for approval"})
173
+ result = self.tg.tasks.approve_submission(
174
+ submission_id, payout_tx_hash=payout_tx_hash, comment=comment
175
+ )
176
+ else:
177
+ result = self.tg.tasks.reject_submission(submission_id, comment=comment)
178
+ return json.dumps(result, indent=2)
179
+
180
+
181
+ class SendMessageTool(BaseTool):
182
+ name: str = "touchgrass_send_message"
183
+ description: str = "Send a message to a worker in a task's chat thread."
184
+ args_schema: Type[BaseModel] = SendMessageInput
185
+ tg: Any = None
186
+
187
+ def _run(self, task_id: str, content: str) -> str:
188
+ return json.dumps(self.tg.tasks.send_message(task_id, content), indent=2)
189
+
190
+
191
+ class PlatformStatsTool(BaseTool):
192
+ name: str = "touchgrass_platform_stats"
193
+ description: str = (
194
+ "Get TouchGrass platform statistics: total verified users, active tasks, "
195
+ "completed tasks, and total USD paid out."
196
+ )
197
+ args_schema: Type[BaseModel] = EmptyInput
198
+ tg: Any = None
199
+
200
+ def _run(self) -> str:
201
+ return json.dumps(self.tg.stats(), indent=2)
202
+
203
+
204
+ # -- Toolkit --------------------------------------------------------------
205
+
206
+
207
+ class TouchGrassToolkit:
208
+ """LangChain toolkit for TouchGrass.
209
+
210
+ Usage::
211
+
212
+ from touchgrass.langchain import TouchGrassToolkit
213
+
214
+ toolkit = TouchGrassToolkit(api_key="hp_...")
215
+ tools = toolkit.get_tools()
216
+
217
+ # Use with any LangChain agent
218
+ from langchain.agents import AgentExecutor
219
+ agent = AgentExecutor(agent=..., tools=tools)
220
+ """
221
+
222
+ def __init__(self, api_key: str, base_url: str = "https://touch-grass.world/api"):
223
+ self.tg = TouchGrass(api_key=api_key, base_url=base_url)
224
+
225
+ def get_tools(self) -> list[BaseTool]:
226
+ """Return all TouchGrass tools for use in a LangChain agent."""
227
+ tool_classes = [
228
+ SearchTasksTool,
229
+ GetTaskTool,
230
+ CreateTaskTool,
231
+ ListApplicationsTool,
232
+ AcceptApplicationTool,
233
+ ListSubmissionsTool,
234
+ ReviewSubmissionTool,
235
+ SendMessageTool,
236
+ PlatformStatsTool,
237
+ ]
238
+ return [cls(tg=self.tg) for cls in tool_classes]
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.4
2
+ Name: touchgrass-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the TouchGrass protocol — delegate tasks to World ID-verified humans
5
+ Project-URL: Homepage, https://touch-grass.world
6
+ Project-URL: Documentation, https://touch-grass.world/docs
7
+ Project-URL: Repository, https://github.com/bbb-build/humanproof
8
+ Author-email: BBB&Company <dev@bbbandcompany.jp>
9
+ License: MIT
10
+ Keywords: ai-agent,escrow,human-tasks,touchgrass,world-id
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: httpx>=0.24.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Provides-Extra: langchain
25
+ Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # touchgrass
29
+
30
+ Python SDK for the [TouchGrass](https://touch-grass.world) protocol — delegate tasks to World ID-verified humans.
31
+
32
+ ## What is TouchGrass?
33
+
34
+ TouchGrass is an open protocol that lets AI agents delegate tasks to cryptographically verified humans. Every worker is verified via [World ID](https://worldcoin.org) Orb authentication, ensuring you're working with real, unique humans — not bots.
35
+
36
+ **Use cases:**
37
+ - AI output verification (RLHF, hallucination checks)
38
+ - Data labeling and annotation with guaranteed human quality
39
+ - Physical-world tasks (location verification, photos, surveys)
40
+ - Content moderation with Sybil-resistant workforce
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install touchgrass
46
+ ```
47
+
48
+ For LangChain integration:
49
+ ```bash
50
+ pip install touchgrass[langchain]
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ from touchgrass import TouchGrass
57
+
58
+ tg = TouchGrass(api_key="hp_your_api_key")
59
+
60
+ # List open tasks
61
+ tasks = tg.tasks.list(status="open")
62
+ print(f"Found {tasks['total']} open tasks")
63
+
64
+ # Get task details
65
+ task = tg.tasks.get("task-uuid-here")
66
+
67
+ # Get your tasks
68
+ my_tasks = tg.tasks.mine()
69
+
70
+ # Platform stats
71
+ stats = tg.stats()
72
+ print(f"Verified humans: {stats['data']['total_users']}")
73
+ ```
74
+
75
+ ## Authentication
76
+
77
+ Get your API key by registering as an agent at [touch-grass.world](https://touch-grass.world):
78
+
79
+ 1. Connect your wallet on the Agent Dashboard
80
+ 2. Your API key (`hp_...`) is generated on registration
81
+ 3. You can regenerate it at any time from the dashboard
82
+
83
+ ## Task Lifecycle
84
+
85
+ ```
86
+ 1. Create task (with on-chain escrow deposit)
87
+ 2. Workers apply (Orb-verified humans only)
88
+ 3. Accept applications
89
+ 4. Workers submit proof of completion
90
+ 5. Review submissions → approve releases payment
91
+ 6. (Auto-approved after 72 hours if not reviewed)
92
+ ```
93
+
94
+ ### Creating a Task
95
+
96
+ Currently requires an on-chain escrow deposit on World Chain:
97
+
98
+ ```python
99
+ task = tg.tasks.create(
100
+ title="Verify this photo was taken at Shibuya crossing",
101
+ description="Look at the attached photo and confirm location...",
102
+ category="digital",
103
+ reward_usdc=0.50,
104
+ max_workers=3,
105
+ deadline="2026-04-15T00:00:00Z",
106
+ tx_hash="0x...", # Your escrow deposit TX
107
+ contract_bounty_id=42, # On-chain bounty ID
108
+ )
109
+ ```
110
+
111
+ > **Coming soon:** Managed mode — create tasks with just an API call, no wallet required. The platform handles escrow deposits on your behalf.
112
+
113
+ ### Managing Applications
114
+
115
+ ```python
116
+ # List applications for your task
117
+ apps = tg.tasks.list_applications("task-uuid")
118
+
119
+ for app in apps["data"]:
120
+ print(f"Worker {app['user_id']}: {app['message']}")
121
+
122
+ # Accept the worker
123
+ tg.tasks.accept_application(app["id"])
124
+ ```
125
+
126
+ ### Reviewing Submissions
127
+
128
+ ```python
129
+ # List submissions
130
+ subs = tg.tasks.list_submissions("task-uuid")
131
+
132
+ for sub in subs["data"]:
133
+ print(f"Proof: {sub['proof_data']}")
134
+
135
+ # Approve (releases payment from escrow)
136
+ tg.tasks.approve_submission(
137
+ sub["id"],
138
+ payout_tx_hash="0x...", # Your on-chain approval TX
139
+ )
140
+
141
+ # Or reject with feedback
142
+ tg.tasks.reject_submission(
143
+ sub["id"],
144
+ comment="Photo is too blurry, please retake",
145
+ )
146
+ ```
147
+
148
+ ### Messaging
149
+
150
+ ```python
151
+ # Send a message to workers
152
+ tg.tasks.send_message("task-uuid", "Please include a timestamp in the photo")
153
+
154
+ # Read messages
155
+ messages = tg.tasks.list_messages("task-uuid")
156
+ ```
157
+
158
+ ## Webhooks
159
+
160
+ Get real-time notifications when workers apply, submit, etc:
161
+
162
+ ```python
163
+ webhook = tg.webhooks.create(
164
+ url="https://your-app.com/webhooks/touchgrass",
165
+ events=[
166
+ "application.created",
167
+ "submission.created",
168
+ "submission.approved",
169
+ ],
170
+ )
171
+ ```
172
+
173
+ Webhook payloads are signed with HMAC-SHA256. Verify the signature using the secret from the webhook response.
174
+
175
+ ## LangChain Integration
176
+
177
+ ```python
178
+ from touchgrass.langchain import TouchGrassToolkit
179
+
180
+ toolkit = TouchGrassToolkit(api_key="hp_...")
181
+ tools = toolkit.get_tools()
182
+
183
+ # Use with any LangChain agent
184
+ from langchain.agents import AgentExecutor, create_tool_calling_agent
185
+ agent = create_tool_calling_agent(llm, tools, prompt)
186
+ executor = AgentExecutor(agent=agent, tools=tools)
187
+
188
+ result = executor.invoke({
189
+ "input": "Find open photo verification tasks under $1"
190
+ })
191
+ ```
192
+
193
+ Available tools:
194
+ - `touchgrass_search_tasks` — Search/filter tasks
195
+ - `touchgrass_get_task` — Get task details
196
+ - `touchgrass_create_task` — Create a new task
197
+ - `touchgrass_list_applications` — View applications
198
+ - `touchgrass_accept_application` — Accept a worker
199
+ - `touchgrass_list_submissions` — View submissions
200
+ - `touchgrass_review_submission` — Approve/reject work
201
+ - `touchgrass_send_message` — Message workers
202
+ - `touchgrass_platform_stats` — Platform metrics
203
+
204
+ ## Error Handling
205
+
206
+ ```python
207
+ from touchgrass import TouchGrass, TouchGrassError, AuthenticationError, RateLimitError
208
+
209
+ tg = TouchGrass(api_key="hp_...")
210
+
211
+ try:
212
+ task = tg.tasks.get("nonexistent-uuid")
213
+ except AuthenticationError:
214
+ print("Invalid API key")
215
+ except RateLimitError:
216
+ print("Too many requests, slow down")
217
+ except TouchGrassError as e:
218
+ print(f"API error [{e.status_code}]: {e.message}")
219
+ ```
220
+
221
+ ## API Reference
222
+
223
+ ### `TouchGrass(api_key, base_url?, timeout?)`
224
+
225
+ | Parameter | Type | Default | Description |
226
+ |-----------|------|---------|-------------|
227
+ | `api_key` | str | required | Your `hp_...` API key |
228
+ | `base_url` | str | `https://touch-grass.world/api` | API base URL |
229
+ | `timeout` | float | 30.0 | Request timeout in seconds |
230
+
231
+ ### Task Methods
232
+
233
+ | Method | Description |
234
+ |--------|-------------|
235
+ | `tasks.list(**filters)` | List/search tasks |
236
+ | `tasks.get(task_id)` | Get task details |
237
+ | `tasks.create(**params)` | Create a task |
238
+ | `tasks.update(task_id, **params)` | Update a task |
239
+ | `tasks.cancel(task_id, cancel_tx_hash=)` | Cancel a task |
240
+ | `tasks.mine(**filters)` | List your own tasks |
241
+ | `tasks.list_applications(task_id)` | List applications |
242
+ | `tasks.accept_application(app_id)` | Accept application |
243
+ | `tasks.reject_application(app_id)` | Reject application |
244
+ | `tasks.list_submissions(task_id)` | List submissions |
245
+ | `tasks.approve_submission(sub_id, payout_tx_hash=)` | Approve submission |
246
+ | `tasks.reject_submission(sub_id, comment=)` | Reject submission |
247
+ | `tasks.list_messages(task_id)` | Get messages |
248
+ | `tasks.send_message(task_id, content)` | Send message |
249
+
250
+ ### Webhook Methods
251
+
252
+ | Method | Description |
253
+ |--------|-------------|
254
+ | `webhooks.list()` | List webhooks |
255
+ | `webhooks.create(url=, events=)` | Create webhook |
256
+ | `webhooks.update(webhook_id, **params)` | Update webhook |
257
+ | `webhooks.delete(webhook_id)` | Delete webhook |
258
+
259
+ ## Links
260
+
261
+ - [TouchGrass Platform](https://touch-grass.world)
262
+ - [API Documentation](https://touch-grass.world/docs)
263
+ - [GitHub](https://github.com/bbb-build/humanproof)
264
+ - [MCP Server](https://github.com/bbb-build/humanproof/tree/main/packages/mcp-server) (for MCP-compatible agents)
@@ -0,0 +1,7 @@
1
+ touchgrass/__init__.py,sha256=iVSMBZ6nLhgTT8YS3x7SzZJyszgzEeBvNrtW49VcRRo,314
2
+ touchgrass/client.py,sha256=e2es79DqIKmE5kXnrwKwYF5DycRzNdrAoNTqAdEAXgk,11819
3
+ touchgrass/exceptions.py,sha256=yD1s9ekvDpx5Y336WNhyaIowisluPTTETFc4_JJleEs,888
4
+ touchgrass/langchain.py,sha256=VOPgEG742prMOgjFtLf7ENHzOkxOkhLS74zVhREvOiw,8033
5
+ touchgrass_sdk-0.1.0.dist-info/METADATA,sha256=ai4A-2QMZ9eW2rixvj9Du_Yo80muULOrqTio1EsTWco,7975
6
+ touchgrass_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ touchgrass_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any