opencode-skills-collection 3.0.36 → 3.0.38
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.
- package/bundled-skills/.antigravity-install-manifest.json +13 -1
- package/bundled-skills/2slides-ppt-generator/SKILL.md +786 -0
- package/bundled-skills/2slides-ppt-generator/references/api-reference.md +499 -0
- package/bundled-skills/2slides-ppt-generator/references/mcp-integration.md +282 -0
- package/bundled-skills/2slides-ppt-generator/references/pricing.md +195 -0
- package/bundled-skills/2slides-ppt-generator/scripts/api_constants.py +87 -0
- package/bundled-skills/2slides-ppt-generator/scripts/create_pdf_slides.py +159 -0
- package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +157 -0
- package/bundled-skills/2slides-ppt-generator/scripts/generate_narration.py +197 -0
- package/bundled-skills/2slides-ppt-generator/scripts/generate_slides.py +247 -0
- package/bundled-skills/2slides-ppt-generator/scripts/get_job_status.py +106 -0
- package/bundled-skills/2slides-ppt-generator/scripts/search_themes.py +137 -0
- package/bundled-skills/anti-sycophancy/README.md +86 -0
- package/bundled-skills/anti-sycophancy/SKILL.md +40 -0
- package/bundled-skills/antigravity-agent-manager/SKILL.md +112 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/sources/sources.md +1 -0
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/event-staffing-compliance/SKILL.md +91 -0
- package/bundled-skills/event-staffing-ordering/SKILL.md +119 -0
- package/bundled-skills/examprep-ai/SKILL.md +446 -0
- package/bundled-skills/hasdata/SKILL.md +107 -0
- package/bundled-skills/hasdata/references/code-recipes.md +150 -0
- package/bundled-skills/hasdata/references/ecommerce.md +116 -0
- package/bundled-skills/hasdata/references/jobs.md +111 -0
- package/bundled-skills/hasdata/references/local-business.md +145 -0
- package/bundled-skills/hasdata/references/real-estate.md +84 -0
- package/bundled-skills/hasdata/references/scraper-jobs.md +252 -0
- package/bundled-skills/hasdata/references/search.md +154 -0
- package/bundled-skills/hasdata/references/travel.md +202 -0
- package/bundled-skills/hasdata/references/web-scraping.md +159 -0
- package/bundled-skills/hasdata/references/youtube.md +186 -0
- package/bundled-skills/hasdata-cli/SKILL.md +169 -0
- package/bundled-skills/hasdata-cli/references/all-commands.md +107 -0
- package/bundled-skills/hasdata-cli/references/ecommerce.md +106 -0
- package/bundled-skills/hasdata-cli/references/enrichment.md +227 -0
- package/bundled-skills/hasdata-cli/references/jobs.md +84 -0
- package/bundled-skills/hasdata-cli/references/local-business.md +123 -0
- package/bundled-skills/hasdata-cli/references/real-estate.md +126 -0
- package/bundled-skills/hasdata-cli/references/search.md +122 -0
- package/bundled-skills/hasdata-cli/references/travel.md +102 -0
- package/bundled-skills/hasdata-cli/references/web-scraping.md +181 -0
- package/bundled-skills/hasdata-cli/references/youtube.md +145 -0
- package/bundled-skills/linkedin-content-generator/SKILL.md +492 -0
- package/bundled-skills/linkedin-content-generator/scripts/generate_calendar.py +82 -0
- package/bundled-skills/linkedin-content-generator/scripts/generate_carousel.py +69 -0
- package/bundled-skills/linkedin-content-generator/scripts/generate_newsletter.py +64 -0
- package/bundled-skills/linkedin-content-generator/scripts/generate_post.py +77 -0
- package/bundled-skills/linkedin-content-generator/scripts/memory.md +49 -0
- package/bundled-skills/linkedin-content-generator/scripts/memory_manager.py +134 -0
- package/bundled-skills/linkedin-content-generator/scripts/utils.py +96 -0
- package/bundled-skills/permission-manager/README.md +22 -0
- package/bundled-skills/permission-manager/SKILL.md +54 -0
- package/bundled-skills/skill-suggester/README.md +14 -0
- package/bundled-skills/skill-suggester/SKILL.md +69 -0
- package/bundled-skills/smart-git-automation/README.md +31 -0
- package/bundled-skills/smart-git-automation/SKILL.md +96 -0
- package/bundled-skills/vercel-optimize/lib/cost-coverage.mjs +3 -1
- package/bundled-skills/vercel-optimize/lib/render-report.mjs +2 -2
- package/bundled-skills/vercel-optimize/lib/util.mjs +7 -0
- package/bundled-skills/vercel-optimize/lib/verify-claim.mjs +2 -7
- package/bundled-skills/vercel-optimize/lib/workspace-resolver.mjs +2 -1
- package/package.json +2 -2
- package/skills_index.json +268 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Code recipes — wiring HasData into your code
|
|
2
|
+
|
|
3
|
+
## Ground rules
|
|
4
|
+
|
|
5
|
+
- **Base URL:** `https://api.hasdata.com`. Header `x-api-key` on every request.
|
|
6
|
+
- **Methods:** Scraper APIs are `GET`; Web Scraping is `POST`; Scraper Jobs use `POST` (submit) + `GET` (status/results) + `DELETE` (stop).
|
|
7
|
+
- **Key handling:** read from env (`HASDATA_API_KEY`). Never hardcode, never log.
|
|
8
|
+
- **Timeouts:** **client timeout ≥ 300 s.** HasData's deadline is 300 s; shorter clients get phantom failures while still being billed.
|
|
9
|
+
- **Retries:** `429` and `5xx` only with exponential backoff + jitter. Never retry `4xx`.
|
|
10
|
+
- **Concurrency:** cap at plan limit. Free tier = 1.
|
|
11
|
+
- **Success signal:** sync APIs require `body.requestMetadata.status === "ok"`. HTTP 200 alone isn't enough.
|
|
12
|
+
|
|
13
|
+
## Status codes
|
|
14
|
+
|
|
15
|
+
| Code | Meaning | Action |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 200 + `status:"ok"` | OK | Use body |
|
|
18
|
+
| 401 | Bad/missing key | Fix — don't retry |
|
|
19
|
+
| 403 | Quota exhausted | Don't retry |
|
|
20
|
+
| 429 | Concurrency cap | Backoff + retry |
|
|
21
|
+
| 500 | Server error | Retry |
|
|
22
|
+
|
|
23
|
+
## Python — minimal client
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import os, requests
|
|
27
|
+
|
|
28
|
+
class HasData:
|
|
29
|
+
BASE = "https://api.hasdata.com"
|
|
30
|
+
|
|
31
|
+
def __init__(self, api_key=None, timeout=300):
|
|
32
|
+
self.s = requests.Session()
|
|
33
|
+
self.s.headers["x-api-key"] = api_key or os.environ["HASDATA_API_KEY"]
|
|
34
|
+
self.timeout = timeout
|
|
35
|
+
|
|
36
|
+
def get(self, path, **params):
|
|
37
|
+
r = self.s.get(f"{self.BASE}{path}", params=params, timeout=self.timeout)
|
|
38
|
+
r.raise_for_status()
|
|
39
|
+
body = r.json()
|
|
40
|
+
if body.get("requestMetadata", {}).get("status") != "ok":
|
|
41
|
+
raise RuntimeError(f"hasdata not-ok: {body.get('requestMetadata')}")
|
|
42
|
+
return body
|
|
43
|
+
|
|
44
|
+
def post(self, path, body):
|
|
45
|
+
r = self.s.post(f"{self.BASE}{path}", json=body, timeout=self.timeout)
|
|
46
|
+
r.raise_for_status()
|
|
47
|
+
return r.json()
|
|
48
|
+
|
|
49
|
+
hd = HasData()
|
|
50
|
+
serp = hd.get("/scrape/google/serp", q="coffee", num=20)["organicResults"]
|
|
51
|
+
md = hd.post("/scrape/web", {"url": "https://example.com", "outputFormat": ["markdown"]})["markdown"]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Python — retry + bounded concurrency
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import time, random
|
|
58
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
59
|
+
from requests import HTTPError
|
|
60
|
+
|
|
61
|
+
def with_retry(fn, attempts=5, base=1.0, cap=60.0):
|
|
62
|
+
for i in range(attempts):
|
|
63
|
+
try:
|
|
64
|
+
return fn()
|
|
65
|
+
except HTTPError as e:
|
|
66
|
+
code = e.response.status_code
|
|
67
|
+
if code == 429 or 500 <= code < 600:
|
|
68
|
+
time.sleep(min(cap, base * 2 ** i) + random.random())
|
|
69
|
+
continue
|
|
70
|
+
raise
|
|
71
|
+
raise RuntimeError("retry exhausted")
|
|
72
|
+
|
|
73
|
+
def scrape_many(urls, workers=5):
|
|
74
|
+
out = {}
|
|
75
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
76
|
+
futs = {ex.submit(lambda u=u: hd.post("/scrape/web", {"url": u, "outputFormat": ["markdown"]})): u
|
|
77
|
+
for u in urls}
|
|
78
|
+
for f in as_completed(futs):
|
|
79
|
+
try:
|
|
80
|
+
out[futs[f]] = f.result().get("markdown")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
out[futs[f]] = e
|
|
83
|
+
return out
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Cap `workers` at your plan's concurrency — anything higher just generates `429`s.
|
|
87
|
+
|
|
88
|
+
## TypeScript — minimal client
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const BASE = "https://api.hasdata.com";
|
|
92
|
+
const KEY = process.env.HASDATA_API_KEY!;
|
|
93
|
+
|
|
94
|
+
async function get<T = any>(path: string, params: Record<string, string | number> = {}): Promise<T> {
|
|
95
|
+
const qs = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)]));
|
|
96
|
+
const r = await fetch(`${BASE}${path}?${qs}`, {
|
|
97
|
+
headers: { "x-api-key": KEY },
|
|
98
|
+
signal: AbortSignal.timeout(300_000),
|
|
99
|
+
});
|
|
100
|
+
if (!r.ok) throw new Error(`HasData ${r.status} ${await r.text()}`);
|
|
101
|
+
const body = await r.json() as any;
|
|
102
|
+
if (body?.requestMetadata?.status && body.requestMetadata.status !== "ok") {
|
|
103
|
+
throw new Error(`HasData not-ok: ${JSON.stringify(body.requestMetadata)}`);
|
|
104
|
+
}
|
|
105
|
+
return body as T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function post<T = any>(path: string, body: unknown): Promise<T> {
|
|
109
|
+
const r = await fetch(`${BASE}${path}`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "x-api-key": KEY, "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
signal: AbortSignal.timeout(300_000),
|
|
114
|
+
});
|
|
115
|
+
if (!r.ok) throw new Error(`HasData ${r.status} ${await r.text()}`);
|
|
116
|
+
return r.json() as Promise<T>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Bounded concurrency, no deps
|
|
120
|
+
async function pool<T, R>(items: T[], n: number, fn: (x: T) => Promise<R>) {
|
|
121
|
+
const out: R[] = []; let i = 0;
|
|
122
|
+
await Promise.all(Array.from({ length: n }, async () => {
|
|
123
|
+
while (i < items.length) { const k = i++; out[k] = await fn(items[k]); }
|
|
124
|
+
}));
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Pagination cheat sheet
|
|
130
|
+
|
|
131
|
+
| Endpoint family | Pagination |
|
|
132
|
+
|---|---|
|
|
133
|
+
| Google SERP / Light SERP / Bing | `start` + `num` (max 100) |
|
|
134
|
+
| Google Maps Search | `start` (steps of 20) |
|
|
135
|
+
| Yelp Search | `start` (steps of 10) |
|
|
136
|
+
| Google Maps Reviews / Glassdoor / Airbnb | `nextPageToken` |
|
|
137
|
+
| Indeed / YellowPages / Amazon Search | `start` or `page` |
|
|
138
|
+
| Shopify Products | `page` (with `limit` ≤ 250) |
|
|
139
|
+
| Scraper-Job results | `page` + `limit` (max 100) until `meta.currentPage >= meta.lastPage` |
|
|
140
|
+
|
|
141
|
+
## Pre-ship checklist
|
|
142
|
+
|
|
143
|
+
- [ ] Key from env, never logged.
|
|
144
|
+
- [ ] All HTTP timeouts ≥ 300 s.
|
|
145
|
+
- [ ] `requestMetadata.status === "ok"` checked on every sync response.
|
|
146
|
+
- [ ] Backoff on 429 + 5xx; never on 4xx.
|
|
147
|
+
- [ ] Concurrency capped at plan limit.
|
|
148
|
+
- [ ] Job `id` (from submit response) persisted to durable storage immediately.
|
|
149
|
+
- [ ] Webhooks paired with polling fallback.
|
|
150
|
+
- [ ] Result files downloaded immediately on `scraper.job.finished`.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# E-commerce APIs — Amazon & Shopify
|
|
2
|
+
|
|
3
|
+
| Endpoint | Returns |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `/scrape/amazon/product` | Single product (price, ratings, variants, other sellers, A+) |
|
|
6
|
+
| `/scrape/amazon/search` | Search results (sponsored + organic) |
|
|
7
|
+
| `/scrape/amazon/seller` | Seller profile |
|
|
8
|
+
| `/scrape/amazon/seller-products` | Seller catalog |
|
|
9
|
+
| `/scrape/shopify/products` | Products from any Shopify store |
|
|
10
|
+
| `/scrape/shopify/collections` | Collections from any Shopify store |
|
|
11
|
+
|
|
12
|
+
All synchronous `GET`.
|
|
13
|
+
|
|
14
|
+
## Amazon Product
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
resp = requests.get(
|
|
20
|
+
"https://api.hasdata.com/scrape/amazon/product",
|
|
21
|
+
headers={"x-api-key": API_KEY},
|
|
22
|
+
params={"asin": "B0DHJ7SBDR", "domain": "www.amazon.com", "otherSellers": "true"},
|
|
23
|
+
timeout=300,
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Param | Notes |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `asin` | **Required.**. |
|
|
30
|
+
| `domain` | `www.amazon.com` (default), `.co.uk`, `.de`, `.co.jp`, … |
|
|
31
|
+
| `language` | Locale per domain. |
|
|
32
|
+
| `deliveryZip` | Affects shipping/availability fields. |
|
|
33
|
+
| `shippingLocation` | 2-letter country code. |
|
|
34
|
+
| `otherSellers` | `true` (default) to include other-seller block. |
|
|
35
|
+
|
|
36
|
+
Response: top-level `requestMetadata` + `product`. The `product` object's keys (verified live): `asin`, `url`, `title`, `brand`, `isAvailable`, `primaryFeatures`, `features`, `featureBullets`, `description`, `badges`, `breadcrumbs`, `whatIsInTheBox`, `variants`, `totalImages`, `primaryImage`, `images`, `descriptionImages`, `totalVideos`, `primaryVideo`, `videos`, `specification`, `reviewsInfo` (rating + count + sample reviews live here, not at the root). Pricing fields are surfaced via `variants` and `specification`.
|
|
37
|
+
|
|
38
|
+
## Amazon Search
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
params = {"q": "mechanical keyboard", "domain": "www.amazon.com", "page": 1}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Params: `q` (required), `domain`, `language`, `page`, `deliveryZip`, `shippingLocation`, `sortBy`.
|
|
45
|
+
|
|
46
|
+
## Amazon Seller / Seller Products
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
profile = requests.get(
|
|
50
|
+
"https://api.hasdata.com/scrape/amazon/seller",
|
|
51
|
+
headers={"x-api-key": API_KEY},
|
|
52
|
+
params={"sellerId": "A1MNOPQR", "domain": "www.amazon.com"},
|
|
53
|
+
timeout=300,
|
|
54
|
+
).json()
|
|
55
|
+
|
|
56
|
+
catalog = requests.get(
|
|
57
|
+
"https://api.hasdata.com/scrape/amazon/seller-products",
|
|
58
|
+
headers={"x-api-key": API_KEY},
|
|
59
|
+
params={"sellerId": "A1MNOPQR", "page": 1},
|
|
60
|
+
timeout=300,
|
|
61
|
+
).json()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use cases: counterfeit detection, MAP enforcement, competitor catalog mirroring.
|
|
65
|
+
|
|
66
|
+
## Shopify Products
|
|
67
|
+
|
|
68
|
+
Works on **any** Shopify storefront with no authentication.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
def shopify_all(store_url):
|
|
72
|
+
page, out = 1, []
|
|
73
|
+
while True:
|
|
74
|
+
batch = requests.get(
|
|
75
|
+
"https://api.hasdata.com/scrape/shopify/products",
|
|
76
|
+
headers={"x-api-key": API_KEY},
|
|
77
|
+
params={"url": store_url, "page": page, "limit": 250},
|
|
78
|
+
timeout=300,
|
|
79
|
+
).json().get("products", [])
|
|
80
|
+
if not batch:
|
|
81
|
+
return out
|
|
82
|
+
out.extend(batch)
|
|
83
|
+
page += 1
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Param | Notes |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `url` | **Required.** Storefront URL. |
|
|
89
|
+
| `limit` | 1–250, default `1`. **Bump to 250** for catalog work. |
|
|
90
|
+
| `page` | 1-indexed. |
|
|
91
|
+
| `collection` | Collection handle filter. |
|
|
92
|
+
|
|
93
|
+
`/scrape/shopify/collections` has the same shape and returns the collection list.
|
|
94
|
+
|
|
95
|
+
## Patterns
|
|
96
|
+
|
|
97
|
+
### Cross-merchant price comparison
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
a = requests.get("https://api.hasdata.com/scrape/amazon/search",
|
|
101
|
+
headers={"x-api-key": API_KEY},
|
|
102
|
+
params={"q": query}, timeout=300).json()
|
|
103
|
+
g = requests.get("https://api.hasdata.com/scrape/google/shopping",
|
|
104
|
+
headers={"x-api-key": API_KEY},
|
|
105
|
+
params={"q": query, "gl": "us"}, timeout=300).json()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Reviews & bestsellers go through Scraper Jobs
|
|
109
|
+
|
|
110
|
+
The Product API only includes a sample of reviews. For all reviews use the `amazon-product-reviews` Scraper Job. For bestseller ranks use `amazon-bestsellers` — there's no synchronous API. See `scraper-jobs.md`.
|
|
111
|
+
|
|
112
|
+
## Gotchas
|
|
113
|
+
|
|
114
|
+
- **Same ASIN ≠ same product across `domain`s.** `.com` vs `.co.uk` can differ.
|
|
115
|
+
- **`deliveryZip` changes availability.** Pass it when stock matters; omit for spec-only scrapes.
|
|
116
|
+
- **Shopify `limit` defaults to 1** — always set 250 for catalog crawls.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Jobs APIs — Indeed & Glassdoor
|
|
2
|
+
|
|
3
|
+
| Endpoint | Returns |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `/scrape/indeed/listing` | Indeed search results |
|
|
6
|
+
| `/scrape/indeed/job` | Single Indeed job detail |
|
|
7
|
+
| `/scrape/glassdoor/listing` | Glassdoor search results |
|
|
8
|
+
| `/scrape/glassdoor/job` | Single Glassdoor job (incl. salary band, company snippet) |
|
|
9
|
+
|
|
10
|
+
All synchronous `GET`.
|
|
11
|
+
|
|
12
|
+
## Indeed Listing
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
resp = requests.get(
|
|
18
|
+
"https://api.hasdata.com/scrape/indeed/listing",
|
|
19
|
+
headers={"x-api-key": API_KEY},
|
|
20
|
+
params={
|
|
21
|
+
"keyword": "software engineer",
|
|
22
|
+
"location": "New York, NY",
|
|
23
|
+
"sort": "date",
|
|
24
|
+
"domain": "www.indeed.com",
|
|
25
|
+
"start": 0,
|
|
26
|
+
},
|
|
27
|
+
timeout=300,
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Param | Notes |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `keyword` | **Required.** |
|
|
34
|
+
| `location` | **Required.** |
|
|
35
|
+
| `sort` | `date`, `relevance` (default). |
|
|
36
|
+
| `domain` | Country site — `www.indeed.com`, `uk.indeed.com`, `de.indeed.com`. |
|
|
37
|
+
| `start` | Offset, **steps of 10**. |
|
|
38
|
+
|
|
39
|
+
Response: `jobs` array with `title`, `company`, `location`, `salary`, `description`, `postedAt`, `link`, `jobKey`. Salary is free-form string — parse with regex.
|
|
40
|
+
|
|
41
|
+
## Indeed Job
|
|
42
|
+
|
|
43
|
+
Pass `jobKey` from listing → returns full description, requirements, benefits, company URL.
|
|
44
|
+
|
|
45
|
+
## Glassdoor Listing & Job
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
params = {"keyword": "software engineer", "location": "New York, NY", "sort": "recent"}
|
|
49
|
+
# pagination: pass back nextPageToken
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Param | Notes |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `keyword`, `location` | **Required.** |
|
|
55
|
+
| `sort` | `recent` (default), `relevant`. |
|
|
56
|
+
| `domain` | Country site. |
|
|
57
|
+
| `nextPageToken` | Cursor pagination. |
|
|
58
|
+
|
|
59
|
+
## Patterns
|
|
60
|
+
|
|
61
|
+
### Salary band
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import re, statistics
|
|
65
|
+
|
|
66
|
+
def salary_band(role, location):
|
|
67
|
+
page = requests.get(
|
|
68
|
+
"https://api.hasdata.com/scrape/indeed/listing",
|
|
69
|
+
headers={"x-api-key": API_KEY},
|
|
70
|
+
params={"keyword": role, "location": location}, timeout=300,
|
|
71
|
+
).json()
|
|
72
|
+
nums = [int(m.replace(",", ""))
|
|
73
|
+
for j in page.get("jobs", [])
|
|
74
|
+
for m in re.findall(r"\$([\d,]+)", j.get("salary") or "")]
|
|
75
|
+
if not nums: return None
|
|
76
|
+
return {"n": len(nums), "median": statistics.median(nums)}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Hiring velocity by company
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from collections import Counter
|
|
83
|
+
|
|
84
|
+
page = indeed_listing(role, loc, sort="date")
|
|
85
|
+
Counter(j.get("company") for j in page.get("jobs", []))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Run weekly; sustained increases often precede earnings/PR signals.
|
|
89
|
+
|
|
90
|
+
### Pagination differs
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Indeed: numeric start
|
|
94
|
+
for p in range(10):
|
|
95
|
+
page = indeed_listing(kw, loc, start=p * 10)
|
|
96
|
+
|
|
97
|
+
# Glassdoor: cursor token
|
|
98
|
+
out, token = [], None
|
|
99
|
+
while True:
|
|
100
|
+
page = glassdoor_listing(kw, loc, next_token=token)
|
|
101
|
+
out.extend(page.get("jobs", []))
|
|
102
|
+
token = page.get("nextPageToken")
|
|
103
|
+
if not token: break
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Gotchas
|
|
107
|
+
|
|
108
|
+
- **Salary is free-form string.** Always regex-parse.
|
|
109
|
+
- **Indeed = numeric start (10), Glassdoor = token.** Don't mix.
|
|
110
|
+
- **`domain` matters for non-US.** `uk.indeed.com`, `ca.indeed.com`, etc.
|
|
111
|
+
- **Prefer the API + pagination for bulk.** Reach for the matching Scraper Job only when you want webhook-driven fan-out across many keyword × location pairs without managing the polling loop yourself.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Local Business APIs — Google Maps, Yelp, YellowPages
|
|
2
|
+
|
|
3
|
+
| Endpoint | Returns |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `/scrape/google-maps/search` | Search results in a viewport |
|
|
6
|
+
| `/scrape/google-maps/place` | Single place details |
|
|
7
|
+
| `/scrape/google-maps/reviews` | Reviews for a place, paginated |
|
|
8
|
+
| `/scrape/google-maps/photos` | Photo gallery |
|
|
9
|
+
| `/scrape/google-maps/posts` | Owner-published posts (offers, events, announcements) |
|
|
10
|
+
| `/scrape/google-maps/contributor-reviews` | All reviews by a Google reviewer |
|
|
11
|
+
| `/scrape/yelp/search` | Yelp search |
|
|
12
|
+
| `/scrape/yelp/place` | Yelp business detail |
|
|
13
|
+
| `/scrape/yellowpages/search` | YellowPages search |
|
|
14
|
+
| `/scrape/yellowpages/place` | YellowPages business detail |
|
|
15
|
+
|
|
16
|
+
All synchronous `GET`.
|
|
17
|
+
|
|
18
|
+
## Google Maps Search
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
resp = requests.get(
|
|
24
|
+
"https://api.hasdata.com/scrape/google-maps/search",
|
|
25
|
+
headers={"x-api-key": API_KEY},
|
|
26
|
+
params={"q": "Pizza", "ll": "@40.7455,-74.0083,14z"},
|
|
27
|
+
timeout=300,
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Param | Notes |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `q` | **Required.** Free-form query. |
|
|
34
|
+
| `ll` | `@LAT,LNG,ZOOMz` viewport — **lat/lng + zoom, not a city name**. Required for tight pagination. |
|
|
35
|
+
| `domain`, `gl`, `hl` | Standard. |
|
|
36
|
+
| `start` | Pagination offset, **steps of 20**. |
|
|
37
|
+
|
|
38
|
+
Response: `localResults` — each entry has `position`, `title`, `placeId`, `dataId`, `kgmid`, `thumbnail`, `phone`, `address`, `website`, `description`, `workingHours` (object with `timezone` + `days[]`), `openState`, `rating`, `reviews`, `type` + `types[]` (categories), `price`, `priceDescription`, `gpsCoordinates`, `serviceOptions[]`, `extensions` (offerings, accessibility, payments, …), `menu`. Feed `placeId`/`dataId` into `/place` and `/reviews`.
|
|
39
|
+
|
|
40
|
+
## Google Maps Place
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
params = {"placeId": "ChIJFU2bda4SM4cRKSCRyb6pOB8"}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Returns full place detail — coordinates, hours by day, phone, website, popular times, attributes (delivery, dine-in), photo summary.
|
|
47
|
+
|
|
48
|
+
## Google Maps Reviews
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
def reviews(place_id=None, data_id=None, sort_by="newestFirst", token=None):
|
|
52
|
+
params = {}
|
|
53
|
+
if place_id: params["placeId"] = place_id
|
|
54
|
+
if data_id: params["dataId"] = data_id
|
|
55
|
+
if sort_by: params["sortBy"] = sort_by
|
|
56
|
+
if token: params["nextPageToken"] = token
|
|
57
|
+
return requests.get(
|
|
58
|
+
"https://api.hasdata.com/scrape/google-maps/reviews",
|
|
59
|
+
headers={"x-api-key": API_KEY},
|
|
60
|
+
params=params, timeout=300,
|
|
61
|
+
).json()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Param | Notes |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `placeId` / `dataId` | Pass one. `dataId` is the hex pair from Maps results. |
|
|
67
|
+
| `sortBy` | `newestFirst`, `highestRating`, `lowestRating`, `mostRelevant`. |
|
|
68
|
+
| `topicId` | Filter by review topic. |
|
|
69
|
+
| `nextPageToken` | Cursor pagination. |
|
|
70
|
+
|
|
71
|
+
## Google Maps Posts
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
resp = requests.get(
|
|
75
|
+
"https://api.hasdata.com/scrape/google-maps/posts",
|
|
76
|
+
headers={"x-api-key": API_KEY},
|
|
77
|
+
params={"placeId": "ChIJ..."}, # or dataId="0x...:0x..."
|
|
78
|
+
timeout=300,
|
|
79
|
+
)
|
|
80
|
+
for p in resp.json().get("posts", []):
|
|
81
|
+
print(p["postedAt"], p["description"][:120], p.get("cta", {}).get("url"))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Either `placeId` **or** `dataId` is required. Optional: `hl` (UI language), `nextPageToken` (cursor pagination). 10 credits/call.
|
|
85
|
+
|
|
86
|
+
Per-post fields (verified live): `postId`, `locationId`, `title`, `description`, `image`, `cta` (`label` + `url`), `createdAt` (ISO), `postedAt` (human-readable), `shareUrl`, `postUrl`. Response top-level: `posts`, `pagination`, `source`, `requestMetadata`.
|
|
87
|
+
|
|
88
|
+
Posts surface current offers, holiday hours, events, and product launches the business is actively promoting. Cheaper signal than the homepage scrape, and `cta.url` is the canonical landing page.
|
|
89
|
+
|
|
90
|
+
## Yelp & YellowPages
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Yelp
|
|
94
|
+
params = {"keyword": "McDonald's", "location": "New York, NY", "start": 0} # steps of 10
|
|
95
|
+
# YellowPages
|
|
96
|
+
params = {"keyword": "Plumbers", "location": "New York, NY", "page": 1}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
YellowPages is US-only — EU/APAC searches return nothing useful.
|
|
100
|
+
|
|
101
|
+
## Patterns
|
|
102
|
+
|
|
103
|
+
### Lead-gen with emails (Maps + Web Scraping)
|
|
104
|
+
|
|
105
|
+
Maps results have website + phone but **not email**. Combine with the Web Scraping API's `extractEmails` only for public business contact pages, legitimate outreach, and workflows that honor opt-out, privacy-law, rate, and terms-of-service constraints:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
leads = []
|
|
109
|
+
for biz in maps_results.get("localResults", []):
|
|
110
|
+
site = biz.get("website")
|
|
111
|
+
if not site: continue
|
|
112
|
+
page = requests.post(
|
|
113
|
+
"https://api.hasdata.com/scrape/web",
|
|
114
|
+
headers={"x-api-key": API_KEY},
|
|
115
|
+
json={"url": site, "extractEmails": True},
|
|
116
|
+
timeout=300,
|
|
117
|
+
).json()
|
|
118
|
+
leads.append({
|
|
119
|
+
"name": biz["title"],
|
|
120
|
+
"phone": biz.get("phone"),
|
|
121
|
+
"website": site,
|
|
122
|
+
"emails": page.get("extractedEmails") or [],
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
For higher volume, switch to the `contacts` Scraper Job (see `scraper-jobs.md`) only when you have a legitimate purpose, a compliant outreach process, and rate/opt-out controls.
|
|
127
|
+
|
|
128
|
+
### New-business discovery
|
|
129
|
+
|
|
130
|
+
Filter Maps by review count `< 5` — usually means recently opened.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
new = [b for b in localResults if (b.get("reviews") or 0) < 5]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Multi-location chain mapping
|
|
137
|
+
|
|
138
|
+
Search the brand name; every `localResults` entry is a branch.
|
|
139
|
+
|
|
140
|
+
## Gotchas
|
|
141
|
+
|
|
142
|
+
- **`ll` is a viewport, not a city.** `@lat,lng,zoom`. Pasting "Brooklyn" fails.
|
|
143
|
+
- **Pagination steps differ.** Maps `start` = +20, Yelp `start` = +10, Maps Reviews uses `nextPageToken`.
|
|
144
|
+
- **`placeId` vs `dataId`** — Place prefers `placeId`; Reviews accepts either.
|
|
145
|
+
- **YellowPages is US-only.**
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Real Estate APIs — Zillow, Redfin
|
|
2
|
+
|
|
3
|
+
| Endpoint | Returns |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `/scrape/zillow/listing` | Search results by area + filters |
|
|
6
|
+
| `/scrape/zillow/property` | Single home (history, agent, schools, taxes) |
|
|
7
|
+
| `/scrape/redfin/listing` | Redfin search results |
|
|
8
|
+
| `/scrape/redfin/property` | Single Redfin home |
|
|
9
|
+
|
|
10
|
+
All synchronous `GET`. 5 credits each.
|
|
11
|
+
|
|
12
|
+
For short-term rentals (Airbnb), hotels (Booking), and flights, see `travel.md`.
|
|
13
|
+
|
|
14
|
+
## Zillow Listing
|
|
15
|
+
|
|
16
|
+
Filter params use **bracketed** keys (`price[min]`, `beds[max]`).
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import requests
|
|
20
|
+
|
|
21
|
+
def zillow_search(keyword, listing_type="forSale", **filters):
|
|
22
|
+
r = requests.get(
|
|
23
|
+
"https://api.hasdata.com/scrape/zillow/listing",
|
|
24
|
+
headers={"x-api-key": API_KEY},
|
|
25
|
+
params={"keyword": keyword, "type": listing_type, **filters},
|
|
26
|
+
timeout=300,
|
|
27
|
+
)
|
|
28
|
+
return r.json()
|
|
29
|
+
|
|
30
|
+
zillow_search("Brooklyn, NY", price={"min": 800000, "max": 2000000})
|
|
31
|
+
zillow_search("33321", "sold", daysOnZillow="6m") # recent comps
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`requests` + `axios` serialize nested dicts as `price[min]=…&price[max]=…` automatically. With raw `URLSearchParams`, build the bracketed keys yourself.
|
|
35
|
+
|
|
36
|
+
| Param | Notes |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `keyword` | **Required.** Area string ("New York, NY", zip, neighborhood). |
|
|
39
|
+
| `type` | **Required.** `forSale`, `forRent`, `sold`. |
|
|
40
|
+
| `price[min/max]`, `beds[min/max]`, `baths[min/max]`, `sqft[min/max]` | Range filters. |
|
|
41
|
+
| `daysOnZillow` | `24h`, `7d`, `14d`, `30d`, `90d`, `6m`, `12m`. |
|
|
42
|
+
| `page` | Pagination. |
|
|
43
|
+
|
|
44
|
+
Response: `requestMetadata`, `searchInformation`, **`properties`** (the listings array — not `listings`), `pagination`.
|
|
45
|
+
|
|
46
|
+
## Zillow Property
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
requests.get(
|
|
50
|
+
"https://api.hasdata.com/scrape/zillow/property",
|
|
51
|
+
headers={"x-api-key": API_KEY},
|
|
52
|
+
params={"url": url, "extractAgentEmails": "true"},
|
|
53
|
+
timeout=300,
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Takes a full Zillow URL (not zpid). Returns address, lot/sqft/beds/baths, price + tax history, schools, agent block, photos. Agent emails are best-effort.
|
|
58
|
+
|
|
59
|
+
## Redfin
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Listing
|
|
63
|
+
params = {"keyword": "33321", "type": "forSale", "page": 1}
|
|
64
|
+
# Property
|
|
65
|
+
params = {"url": "https://www.redfin.com/FL/Tamarac/9...html"}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Same bracketed `price[min]`, `beds[min]`, etc. as Zillow. Zip codes work best for `keyword`.
|
|
69
|
+
|
|
70
|
+
## Patterns
|
|
71
|
+
|
|
72
|
+
### Sold comps for ROI
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
sold = zillow_search(zip_code, "sold", daysOnZillow="6m").get("properties", [])
|
|
76
|
+
ppsf = [(l["price"] / l["livingArea"]) for l in sold if l.get("livingArea")]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Gotchas
|
|
80
|
+
|
|
81
|
+
- **Bracketed query keys** — work with `requests`/`axios`, not raw `URLSearchParams`.
|
|
82
|
+
- **`type=sold` + `daysOnZillow` = comps recipe.** Without `daysOnZillow`, history is unbounded.
|
|
83
|
+
- **Property endpoints take URLs**, not IDs.
|
|
84
|
+
- **Agent emails are best-effort.**
|