diff --git a/backend/api_core.py b/backend/api_core.py index c71d889..9999316 100644 --- a/backend/api_core.py +++ b/backend/api_core.py @@ -903,6 +903,32 @@ def clear_login_failures(email): 'untraceable', 'anonymous task', ] +PAYMENT_CIRCUMVENTION_PATTERNS = [ + r"\bpaypal\.me/\S+", + r"\bpaypal\b", + r"\bvenmo\b", + r"\bcash\s*app\b", + r"\bcashapp\b", + r"\bzelle\b", + r"\bcrypto\b", + r"\bbitcoin\b", + r"\bethereum\b", + r"\bsolana\b", + r"\bevm\b", + r"\bwallet\b", + r"\bdirect\s+payment\b", + r"\boff[-\s]?platform\s+payment\b", + r"\bpay\s+me\s+direct\b", + r"\bsend\s+payment\s+to\b", +] + + +def check_payment_circumvention(text): + for pattern in PAYMENT_CIRCUMVENTION_PATTERNS: + if re.search(pattern, text or "", re.IGNORECASE): + return False, "Payment instructions must stay on-platform. Do not include direct payment links, wallet addresses, or off-platform payment instructions." + return True, None + VALID_CATEGORIES = [ 'web_development', 'mobile_development', 'software_development', 'graphic_design', 'ui_ux_design', 'video_editing', 'photography', @@ -2026,6 +2052,15 @@ def _handle_routes(db): return error_response(f"Invalid category. Must be one of: {', '.join(VALID_CATEGORIES)}") safe, msg = check_content_safety(body['title'] + " " + body['description']) + if not safe: + return error_response(f"Service rejected: {msg}", 422) + service_text = " ".join([ + str(body.get('title') or ''), + str(body.get('description') or ''), + str(body.get('includes') or ''), + " ".join(body.get('tags') or []) if isinstance(body.get('tags'), list) else str(body.get('tags') or ''), + ]) + safe, msg = check_payment_circumvention(service_text) if not safe: return error_response(f"Service rejected: {msg}", 422) @@ -2099,6 +2134,15 @@ def _handle_routes(db): safe, msg = check_content_safety(txt) if not safe: return error_response(f"Service update rejected: {msg}", 422) + merged_service_text = " ".join([ + str(body.get('title') if 'title' in body else svc['title'] or ''), + str(body.get('description') if 'description' in body else svc['description'] or ''), + str(body.get('includes') if 'includes' in body else svc['includes'] or ''), + " ".join(body.get('tags') or []) if isinstance(body.get('tags'), list) else str(body.get('tags') if 'tags' in body else svc['tags'] or ''), + ]) + safe, msg = check_payment_circumvention(merged_service_text) + if not safe: + return error_response(f"Service update rejected: {msg}", 422) updates = [] vals = [] diff --git a/backend/test_deep_audit_regressions.py b/backend/test_deep_audit_regressions.py index 6985604..e422878 100644 --- a/backend/test_deep_audit_regressions.py +++ b/backend/test_deep_audit_regressions.py @@ -257,6 +257,68 @@ def test_api_key_header_authenticates_protected_profile_route(self): finally: db.close() + def test_service_creation_rejects_off_platform_payment_instructions(self): + db = self.module.get_db() + token = "tok-worker" + try: + db.execute("INSERT INTO users (id,email,password_hash,name) VALUES (1,'worker@example.com','x','Worker')") + db.execute("INSERT INTO sessions (user_id,token,expires_at) VALUES (1,?,datetime('now','+1 day'))", [token]) + db.commit() + finally: + db.close() + + payload = { + "title": "Website QA pass", + "description": "I can test your website. Direct payment via PayPal is available.", + "category": "testing", + "pricing_type": "fixed", + "price": 25, + "includes": "Send payment to a crypto wallet before work starts", + } + body = json.dumps(payload) + self.module._request_ctx.request_method = "POST" + self.module._request_ctx.path_info = "/api/v1/services" + self.module._request_ctx.query_string = "" + self.module._request_ctx.http_authorization = f"Bearer {token}" + self.module._request_ctx.http_x_api_key = "" + self.module._request_ctx.stdin_data = body + self.module._request_ctx.content_type = "application/json" + self.module._request_ctx.content_length = str(len(body)) + self.module._request_ctx.remote_addr = "127.0.0.1" + with contextlib.redirect_stdout(io.StringIO()) as out: + self.module.handle_request() + status, response = parse_cgi_output(out.getvalue()) + self.assertEqual(status, 422, response) + self.assertIn("Payment instructions must stay on-platform", response.get("error", "")) + + def test_service_update_rejects_off_platform_payment_instructions_in_tags(self): + db = self.module.get_db() + token = "tok-worker" + try: + db.execute("INSERT INTO users (id,email,password_hash,name) VALUES (1,'worker@example.com','x','Worker')") + db.execute("INSERT INTO sessions (user_id,token,expires_at) VALUES (1,?,datetime('now','+1 day'))", [token]) + db.execute("INSERT INTO services (id,worker_id,title,description,category,pricing_type,price,status) VALUES (1,1,'Testing Svc','Clean QA scope','testing','fixed',25,'active')") + db.commit() + finally: + db.close() + + payload = {"tags": ["qa", "solana wallet accepted"]} + body = json.dumps(payload) + self.module._request_ctx.request_method = "PUT" + self.module._request_ctx.path_info = "/api/v1/services/1" + self.module._request_ctx.query_string = "" + self.module._request_ctx.http_authorization = f"Bearer {token}" + self.module._request_ctx.http_x_api_key = "" + self.module._request_ctx.stdin_data = body + self.module._request_ctx.content_type = "application/json" + self.module._request_ctx.content_length = str(len(body)) + self.module._request_ctx.remote_addr = "127.0.0.1" + with contextlib.redirect_stdout(io.StringIO()) as out: + self.module.handle_request() + status, response = parse_cgi_output(out.getvalue()) + self.assertEqual(status, 422, response) + self.assertIn("Payment instructions must stay on-platform", response.get("error", "")) + def test_job_creation_notifies_matching_service_workers(self): db = self.module.get_db() token = "tok-employer" @@ -1347,13 +1409,13 @@ def test_public_nav_active_state_uses_light_pill_for_all_tabs(self): def test_sitemapped_html_pages_use_single_canonical_public_nav(self): expected_labels = [ "GoHireHumans", + "Starter QA Offers", "Marketplace", - "Open Jobs", + "Open Jobs for Workers", "For Agents", "Agent Guide", "Use Cases", "About", - "FAQ", ] failures = [] for rel in self._sitemapped_html_pages(): @@ -1448,10 +1510,11 @@ def _assert_shared_landing_nav(self, pages): '', '
', '
`; @@ -1263,10 +1272,10 @@

GoHireHumans routes AI outputs, automations, data, and real-world checks to humans for fixed-scope QA and verification reports.

Founding offers - AI Output Verification $99 • Automation QA Sprint $199 • Real-World Check $79. + AI Output Verification $99 • Automation QA Sprint $199 • Clay/GTM QA Sprint $199 • Real-World Check $79.
@@ -1281,6 +1290,7 @@

+

@@ -1383,7 +1393,7 @@

Use it when work needs a clear human or agent helper

Start with a small, proof-backed QA sprint.

-

Pick one founding offer: AI Output Verification, Automation QA Sprint, or Real-World Check. Each is fixed-scope, buyer-reviewed before payment, and designed to produce a clear evidence pack.

+

Pick one founding offer: AI Output Verification, Automation QA Sprint, Clay/GTM QA Sprint, or Real-World Check. Each is fixed-scope, buyer-reviewed before payment, and designed to produce a clear evidence pack.

Transaction proof
@@ -1649,27 +1659,27 @@

Browse Services

@@ -2012,20 +2022,20 @@

Browse Jobs