
Linkfox Sif Asin Keywords
- 233 installs
- 64 repo stars
- Updated August 3, 2026
- linkfox-ai/linkfox-skills
Helps with ai & agent building tasks.
About
linkfox-sif-asin-keywords is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- linkfox-sif-asin-keywords
- AI & Agent Building
- AI-coding skill
Linkfox Sif Asin Keywords by the numbers
- 233 all-time installs (skills.sh)
- +35 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #2,666 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/linkfox-ai/linkfox-skills --skill linkfox-sif-asin-keywordsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 233 |
|---|---|
| repo stars | ★ 64 |
| Last updated | August 3, 2026 |
| Repository | linkfox-ai/linkfox-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
SIF ASIN Keyword Analysis
This skill guides you on how to query and analyze traffic keywords for a specific Amazon ASIN, helping Amazon sellers understand which keywords drive traffic to a product and how that product ranks for each keyword.
Core Concepts
SIF ASIN Keyword data reveals the keywords that bring traffic to a specific Amazon product (ASIN). For each keyword, you can see the product's organic search rank, SP ad rank, search volume, traffic share, display position types, and various performance markers. This is the go-to tool for reverse ASIN keyword lookup.
Single-ASIN limitation: This tool queries one ASIN at a time. If the user wants to compare multiple ASINs, you must make separate queries for each.
Ranking logic: A smaller rank value means a better (higher) position. Rank 1 means the product appears first in search results. When a user says "ranking improved", the numeric value decreased; "ranking dropped" means the value increased.
Data Fields
Per-keyword record (each element of data)
| Field | API Name | Description | Example |
|---|---|---|---|
| Keyword | keyword | The search keyword driving traffic | wireless charger |
| Keyword Translation | translateKeyword | Localized keyword translation for the marketplace | 无线充电器 |
| ASIN | asin | The product ASIN being queried | B0XXXXXXXX |
| Organic Rank | productNaturalRank | Product's position in organic search results | 5 |
| Organic Rank (Display) | naturalRankDisplay | Organic rank as display text | 5 |
| Ad Rank | productAdRank | Product's position in SP ad results | 3 |
| Ad Rank (Display) | adRankDisplay | Ad rank as display text | 3 |
| Weekly Search Volume | weeklySearchVolume | Estimated weekly searches for this keyword | 125000 |
| Keyword Popularity Rank | keywordPopularityRank | Keyword's search volume rank among all keywords (lower = more popular) | 203 |
| Total Search Result Products | totalSearchResultProductCount | Total products shown under this keyword (organic + ads + recommendations) | 1280 |
| Traffic Share | trafficShare | Share of traffic this keyword contributes to the ASIN (1 = 100%) | 0.05 |
| Natural Traffic Share | naturalTrafficShare | Organic exposure score / total score | 0.62 |
| Paid Traffic Share | paidTrafficShare | Paid-ad exposure score / total score (SP + SB + SBV + recAd) | 0.31 |
| Natural Traffic Score | naturalTrafficScore | Organic search exposure score for this ASIN on this keyword (0 = none) | 4.2 |
| SP Ads Score | sponsoredProductsScore | Sponsored Products regular-slot score (excludes SP recommendation slots) | 2.1 |
| Brand Ad (SB) Score | brandAdScore | Sponsored Brands total score (standard + video) | 0.8 |
| Video Ad (SBV) Score | videoAdScore | Sponsored Brands Video score | 0.3 |
| SP Recommendation Score | sponsoredRecommendationScore | Combined score across SP recommendation slots (Trending now, Seen on social media, Customers frequently viewed, 4 stars and above, etc.) | 1.4 |
| SP Recommendation Breakdown | sponsoredRecommendationBreakdown | Array of {title, score, scoreRatio} per SP recommendation slot | [{"title":"Trending now","score":0.8,"scoreRatio":0.57}] |
| ABA TOP3 Click Concentration | clickConcentrationShare | Whether clicks under this keyword concentrate on the top ASINs (NOT a conversion rate) | 0.42 |
| Click-to-Purchase Conversion | clickToPurchaseConversionRate | purchaseQty / clickQty at the keyword level | 0.037 |
| Display Position Types | displayPositionTypes | Where the product appears: natural, ac, sp, top, bottom, er, vedio, tr, trfob | ["natural", "sp"] |
| Traffic Characteristic Markers | trafficCharacteristicMarkers | Traffic feature tags: isMainKw, isAccurateKw, isAccurateAboveKw, isAccurateTailKw | ["isMainKw"] |
| Conversion Performance Markers | conversionPerformanceMarkers | Conversion tags: isPurchaseKw, isQualityKw, isStableKw, isLossKw, isInvalidKw | ["isPurchaseKw"] |
| Last Organic Rank Time | lastNaturalRankTime | When the product last had a valid organic rank for this keyword | 2026-04-20 |
| Last Ad Rank Time | lastAdRankTime | When the product last had a valid SP ad rank for this keyword | 2026-04-20 |
| Period End Date | periodEndDate | End date of the current (weekly) period = start-week + 7 days | 2026-04-27 |
| Update Time | updateTime | When the keyword data was last updated | 2026-04-21 |
Top-level response fields (alongside data)
| Field | API Name | Description |
|---|---|---|
| Is Parent ASIN | isParentAsin | Whether the queried ASIN is a parent (variation hub) |
| Has Variants | hasVaiants | Whether the ASIN has variants |
| Latest ABA Week | abaCreateDateWeek | Latest ABA weekly data reference date |
Supported Marketplaces
13 marketplaces: US (United States), UK (United Kingdom), DE (Germany), CA (Canada), JP (Japan), FR (France), ES (Spain), IT (Italy), MX (Mexico), AU (Australia), AE (United Arab Emirates), BR (Brazil), SA (Saudi Arabia).
Default marketplace is US. Use US when the user does not specify a marketplace. Codes outside this list will be rejected by the API pattern.
API Usage
This tool calls the LinkFox tool gateway API. See references/api.md for calling conventions, request parameters, and response structure. You can also execute scripts/sif_asin_keywords.py directly to run queries.
Parameter Guide
Required Parameter
- asin (string, required): The Amazon ASIN to look up. Only one ASIN per request.
Optional Parameters
- country (string, default
US): Marketplace code. See Supported Marketplaces above. - keyword (string): Filter results to keywords containing this text. Translate the keyword into the language of the target marketplace when possible.
- timePieceType (string, default
latelyDay): Time window type —latelyDay(most recent N days),month(specific month),week(specific week). - timePieceValue (string, default
7): Value paired withtimePieceType. - When
timePieceType=latelyDay→ only7or30are supported - When
timePieceType=month→YYYY-MM(e.g.2026-04) - When
timePieceType=week→ the week's start dateYYYY-MM-DD(e.g.2026-04-13) - conditions (string): Comma-separated condition filters. Flag-style filters:
nfPosition-- organic traffic keywordsisSpAd-- SP ad keywordsisBrandAd-- brand ad keywordsisVedioAd-- video ad keywordsisAC-- Amazon's Choice keywordsisAccurateKw-- precise traffic keywordsisAccurateTailKw-- precise long-tail keywordsisPurchaseKw-- purchase-converting keywordsisQualityKw-- high-quality conversion keywordsisStableKw-- stable conversion keywordsisLossKw-- conversion-loss keywordsisInvalidKw-- invalid-exposure keywordsisMultiVariantKw-- keywords ranking organically across multiple variantsisSearchVolUpKw-- keywords whose search volume increased year-over-yearisSearchVolDownKw-- keywords whose search volume decreased year-over-year
Period-count filters (all / new-in):
totalPeriod.in-- newly-entered traffic keywords this periodnfKeywordCnt.total/nfKeywordCnt.in-- keywords with (new) organic exposureadKeywordCnt.total/adKeywordCnt.in-- keywords with (new) ad exposureallSpKeywordCnt.total/allSpKeywordCnt.in-- (new) SP-ad keywords (regular + recommendation)spKeywordCnt.total/spKeywordCnt.in-- (new) SP regular keywordsrecSpKeywordCnt.total/recSpKeywordCnt.in-- (new) SP recommendation keywordsallSbKeywordCnt.total/allSbKeywordCnt.in-- (new) SB-ad keywordssbKeywordCnt.total/sbKeywordCnt.in-- (new) SB regular keywordssbvKeywordCnt.total/sbvKeywordCnt.in-- (new) SBV keywords- sortBy (string): Sort field. Options:
lastRank(organic rank),adLastRank(ad rank),updateTime(update time),searchesRank(search popularity rank),estSearchesNum(monthly search volume). - desc (boolean, default
true): Sort in descending order. Set tofalsefor ascending. - pageNum (integer, default
1): Page number for pagination. - pageSize (integer, default
100, range 10-100): Number of results per page.
Building Effective Queries
1. Always specify the marketplace: Set country to one of the 13 supported codes. 2. Pick the right time window: The default is the latest 7 days. Pass timePieceType=month + timePieceValue=YYYY-MM for a specific month, or timePieceType=week + timePieceValue=YYYY-MM-DD for a specific ABA week. 3. Use keyword filtering: When the user is interested in specific keywords, pass the keyword parameter to narrow results. 4. Apply condition filters: Use conditions to focus on specific keyword types (e.g., only organic keywords, only purchase-converting keywords, or newly-entered SP keywords via spKeywordCnt.in). 5. Choose appropriate sorting: Sort by estSearchesNum for highest search volume keywords, or by lastRank for best-ranking keywords. 6. Handle pagination: The API returns at most 100 results per page. Use pageNum to retrieve additional pages if the total exceeds 100.
Usage Examples
1. Find all traffic keywords for an ASIN on the US marketplace
asin: "B0XXXXXXXX", country: "US"2. Find organic traffic keywords only
asin: "B0XXXXXXXX", country: "US", conditions: "nfPosition"3. Find keywords containing "charger" sorted by search volume (ascending)
asin: "B0XXXXXXXX", country: "US", keyword: "charger", sortBy: "estSearchesNum", desc: false4. Find high-converting keywords
asin: "B0XXXXXXXX", country: "US", conditions: "isPurchaseKw,isQualityKw"5. Find SP ad keywords on the Japan marketplace
asin: "B0XXXXXXXX", country: "JP", conditions: "isSpAd", sortBy: "adLastRank", desc: false6. Find precise long-tail keywords with stable conversion
asin: "B0XXXXXXXX", country: "US", conditions: "isAccurateTailKw,isStableKw"7. Find newly-entered SP traffic keywords in April 2026
asin: "B0XXXXXXXX", country: "US", timePieceType: "month", timePieceValue: "2026-04", conditions: "spKeywordCnt.in"8. Find keywords whose search volume is trending up
asin: "B0XXXXXXXX", country: "US", conditions: "isSearchVolUpKw", sortBy: "estSearchesNum", desc: trueDisplay Rules
1. Present data only: Show query results in clear tables without subjective business advice. 2. Ranking clarification: When showing ranking data, remind users that lower numeric values mean better (higher) positions. 3. Share formatting: Display trafficShare, naturalTrafficShare, paidTrafficShare, and clickConcentrationShare as percentages (multiply by 100). For example, 0.05 should be shown as 5%. 4. Click concentration wording: clickConcentrationShare measures how concentrated clicks are on top ASINs under the keyword — it is NOT a conversion rate. Label it clearly so users don't confuse it with clickToPurchaseConversionRate. 5. Period disclosure: When showing counts sourced from *.in/*.total filters or comparing numbers, annotate the period range — default is last 7 days; if timePieceType=month or week is set, surface the resolved period (periodEndDate / abaCreateDateWeek). 6. Marker translation: Translate marker arrays into human-readable labels. For example, ["isMainKw", "isAccurateKw"] should display as "Main Traffic, Precise Traffic". 7. Display position translation: Translate position type arrays into readable labels: natural = Organic, ac = Amazon's Choice, sp = SP Ad, top = Top Brand Ad, bottom = Bottom Brand Ad, er = Editorial Recommendation, vedio = Video Ad, tr = Top Rated, trfob = Top Rated Frequently Bought. 8. Pagination notice: When results have more pages, inform the user of the total count and suggest fetching additional pages. 9. Error handling: When a query fails, explain the reason based on the msg field and suggest adjusting query parameters. 10. Multi-ASIN requests: If the user asks about multiple ASINs, make separate API calls for each and present the results together.
Important Limitations
- Single ASIN per request: Only one ASIN can be queried at a time.
- Page size cap: Maximum 100 results per page.
- Time window granularity:
timePieceType=latelyDayonly supportstimePieceValue=7or30; arbitrary N-day windows are not supported. - Marketplace coverage: 13 marketplaces only — IN / NL / SE / PL / TR / SG are no longer available.
- Keyword language: The
keywordfilter should ideally be in the language of the target marketplace.
User Expression & Scenario Quick Reference
Applicable -- Keyword analysis for specific Amazon products:
| User Says | Scenario |
|---|---|
| "What keywords does this ASIN rank for" | Reverse ASIN keyword lookup |
| "Show me the traffic keywords for B0XXX" | Traffic keyword analysis |
| "What's the organic rank for this product" | Organic ranking check |
| "Which keywords is this product advertising on" | Ad keyword analysis |
| "Find high-converting keywords for this ASIN" | Conversion keyword mining |
| "What are the main traffic sources for this product" | Main traffic keyword identification |
| "Show me keywords with lost conversions" | Conversion loss diagnosis |
| "Which keywords have Amazon's Choice badge" | AC keyword discovery |
| "Compare keyword rankings for my ASIN" | Keyword position analysis |
| "Which new SP keywords did this ASIN get last month" | New-in period keyword discovery (spKeywordCnt.in + month window) |
| "Keywords whose search volume is trending up YoY" | Search-volume trend filter (isSearchVolUpKw) |
| "Is click concentration high on this keyword" | ABA TOP3 click concentration read |
| "Keyword click-to-purchase conversion" | Per-keyword conversion check |
Not applicable -- Needs beyond single-ASIN keyword lookup:
- Broad keyword research not tied to a specific ASIN (use ABA data tools instead)
- Product reviews, listing copywriting
- Sales estimation, revenue analysis
- Advertising campaign management (bids, budgets)
- Category-wide keyword trends without a specific ASIN
Feedback:
Auto-detect and report feedback via the Feedback API when any of the following apply: 1. The functionality or purpose described in this skill does not match actual behavior 2. The skill's results do not match the user's intent 3. The user expresses dissatisfaction or praise about this skill 4. Anything you believe could be improved
Call the feedback API as specified in references/api.md. Do not interrupt the user's flow.
<!-- LF_LARGE_RESPONSE_BLOCK -->
Handling Large Responses
To avoid overflowing the agent context, persist the response to disk and extract only the fields you need:
python scripts/response_io.py run --script scripts/sif_asin_keywords.py --out-dir <DIR> '<params>'
python scripts/response_io.py read <file> --fields "<paths>" # or --path "<JMESPath>"Pick--out-diroutside any git working tree (e.g./tmp/...on Unix,%TEMP%/...on Windows). Persisted responses may contain PII, pricing, or auth-sensitive data — do not commit them. Files are not auto-deleted; clean up when the task is done.
run writes the full response to a file and emits only a schema preview + file path. read projects specific fields, with --limit/--offset for slicing and --format json|jsonl|csv|table for output.
When to prefer this pattern — apply your judgment based on the response characteristics, e.g.:
- High field count per record, or fields you don't need
- Batch/paginated results (multiple items per call)
- Long-text fields (descriptions, reviews, HTML, time series)
- Output reused across later steps rather than consumed immediately
For small, single-use responses, calling the main script directly is fine.
⚠️ The preview is a truncated schema + sample, not the full data. Any field-level decision must read from the persisted file via read. <!-- /LF_LARGE_RESPONSE_BLOCK -->
--- For more high-quality, professional cross-border e-commerce skills, set [LinkFox Skills](https://skill.linkfox.com/).
SIF-ASIN的关键词 API 参考
调用规范
- 请求地址:
https://tool-gateway.linkfox.com/sif/asinKeywords - 请求方式:POST,Content-Type: application/json
- 认证方式:Header
Authorization: <api_key>,api_key 从环境变量LINKFOXAGENT_API_KEY读取(如未配置,提示用户前往 https://skill.linkfox.com/linkfoxskills/guide.htm 申请)
请求参数
POST Body(JSON):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| asin | string | 是 | ASIN码,最大长度1000字符。本工具一次只能查询一个ASIN |
| country | string | 否 | 国家站点,默认 US。可选值(共 13 个):US、UK、DE、CA、JP、FR、ES、IT、MX、AU、AE、BR、SA |
| keyword | string | 否 | 关键词,最大长度1000。尽量翻译成对应国家站点的语言 |
| timePieceType | string | 否 | 时间片段类型,默认 latelyDay。可选值:latelyDay(最近N天)、month(某月)、week(某周) |
| timePieceValue | string | 否 | 时间片段值,默认 7,最大长度1000。latelyDay 时仅支持 7 或 30;month 时为 YYYY-MM(如 2026-04);week 时为周开始日期 YYYY-MM-DD(如 2026-04-13) |
| conditions | string | 否 | 条件筛选,多个以英文逗号隔开。可选值:<br>标志类:nfPosition(自然流量词)、isSpAd(SP广告词)、isBrandAd(品牌广告词)、isVedioAd(视频广告词)、isAC(AC推荐词)、isAccurateKw(精准流量词)、isAccurateTailKw(精准长尾词)、isPurchaseKw(出单词)、isQualityKw(转化优质词)、isStableKw(转化平稳词)、isLossKw(转化流失词)、isInvalidKw(无效曝光词)、isMultiVariantKw(多变体自然位词)、isSearchVolUpKw(搜索量同比增长词)、isSearchVolDownKw(搜索量同比下降词)<br>周期计数类(`.total` 全量 / `.in` 新进):totalPeriod.in、nfKeywordCnt.total、nfKeywordCnt.in、adKeywordCnt.total、adKeywordCnt.in、allSpKeywordCnt.total、allSpKeywordCnt.in、spKeywordCnt.total、spKeywordCnt.in、recSpKeywordCnt.total、recSpKeywordCnt.in、allSbKeywordCnt.total、allSbKeywordCnt.in、sbKeywordCnt.total、sbKeywordCnt.in、sbvKeywordCnt.total、sbvKeywordCnt.in |
| sortBy | string | 否 | 排序字段。可选值:lastRank(自然排名)、adLastRank(广告排名)、updateTime(关键词抓取时间)、searchesRank(搜索排名)、estSearchesNum(月搜索量)。空字符串为默认系统排序 |
| desc | boolean | 否 | 是否降序,默认 true |
| pageNum | integer | 否 | 页码,默认 1 |
| pageSize | integer | 否 | 每页数量,最小10,最大100,默认 100 |
响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
| code | string | 返回码 |
| msg | string | 消息 |
| total | integer | 本次实际返回的数据数量 |
| data | array | 返回数据数组(详见下方) |
| columns | array | 渲染的列 |
| type | string | 渲染的样式 |
| title | string | 标题 |
| isParentAsin | boolean | 是否是父体(pasin) |
| hasVaiants | boolean | 是否有变体 |
| abaCreateDateWeek | string | 最新周 ABA 数据对应的周时间 |
| costTime | integer | 耗时(ms) |
| costToken | integer | 消耗token |
data 数组元素字段
| 字段 | 类型 | 说明 |
|---|---|---|
| keyword | string | 关键词 |
| translateKeyword | string | 关键词翻译,站点本地化译文 |
| asin | string | 商品ASIN |
| productNaturalRank | integer | 商品自然搜索排名。该商品在此关键词下的自然搜索结果中的位置排名,如1表示排在搜索结果第1位(首位) |
| naturalRankDisplay | string | 自然排名显示文本。自然搜索排名的字符串表示形式 |
| productAdRank | integer | 商品SP广告排名。该商品在此关键词下的Sponsored Products广告位中的排名位置,如3表示排在广告位第3位 |
| adRankDisplay | string | 广告排名显示文本。SP广告排名的字符串表示形式 |
| weeklySearchVolume | integer | 周搜索量。该关键词在亚马逊平台每周的预估搜索次数 |
| keywordPopularityRank | integer | 关键词搜索热度排名。该关键词的月搜索量在亚马逊所有关键词中的排名,数值越小表示搜索量越大 |
| totalSearchResultProductCount | integer | 该关键词下搜索结果商品总数(在售产品数) |
| trafficShare | number | 流量占比。该关键词为商品带来的流量占所有关键词总流量的比例,其中1表示100% |
| naturalTrafficShare | number | 自然流量得分占比。自然搜索流量得分 / 总得分 |
| paidTrafficShare | number | 付费广告流量得分占比。广告流量得分 / 总得分;广告合计 = sp + sb + sbv + recAd |
| naturalTrafficScore | number | 自然流量得分。该关键词为该 ASIN 带来的自然搜索曝光得分,0 = 无自然流量曝光 |
| sponsoredProductsScore | number | SP 广告常规得分。Sponsored Products 常规位的流量得分(不含 SP 推荐位) |
| brandAdScore | number | SB 品牌广告得分。Sponsored Brands 品牌广告的流量得分(常规 + 视频,总和) |
| videoAdScore | number | SBV 视频广告得分。Sponsored Brands Video 视频广告的流量得分 |
| sponsoredRecommendationScore | number | SP 推荐位得分。Trending now / Seen on social media / Customers frequently viewed / 4 stars and above 等合计得分 |
| sponsoredRecommendationBreakdown | array | SP 推荐位得分明细。每项 {title, score, scoreRatio} |
| clickConcentrationShare | number | ABA TOP3 点击集中度。衡量点击是否集中在头部 ASIN;注意不是转化率 |
| clickToPurchaseConversionRate | number | 点击到购买的转化率(purchaseQty / clickQty) |
| displayPositionTypes | array | 商品展示位置类型数组。可能包含以下值:natural=自然搜索结果位;ac=Amazon's Choice推荐位;sp=Sponsored Products赞助商品广告位;top=页面顶部品牌广告位;bottom=页面底部品牌广告位;er=Editorial Recommendations编辑推荐位;vedio=视频广告位;tr=Top Rated高评分推荐位;trfob=Top Rated Frequently Bought高频购买推荐位 |
| trafficCharacteristicMarkers | array | 关键词流量特征标记数组。可能包含以下值:isMainKw=主要流量词;isAccurateKw=精准流量词;isAccurateAboveKw=精准大词;isAccurateTailKw=精准长尾词 |
| conversionPerformanceMarkers | array | 转化效果标记数组。可能包含以下值:isPurchaseKw=出单词;isQualityKw=转化优质词;isStableKw=转化平稳词;isLossKw=转化流失词;isInvalidKw=无效曝光词 |
| lastNaturalRankTime | string | 最近有效自然排名的时间 |
| lastAdRankTime | string | 最近有效SP广告排名的时间 |
| periodEndDate | string | 本周期(周粒度)结束日期 = 开始周 + 7 天(站点时间) |
| updateTime | string | 关键词数据更新时间 |
错误码
正常情况下,接口的 HTTP 状态码均为 200,业务的成功与否通过响应体中的 errorCode 字段区分(errorCode = 200 表示成功,其他值表示业务错误)。当遇到未授权等情况时,HTTP 状态码为 401,且对应的 errorCode 也是 401。
| errcode | 含义 | 处理建议 |
|---|---|---|
| 200 | 成功 | 正常解析业务字段 |
| 401 | 认证失败 | 检查请求头 Authorization 是否正确携带 API Key;API Key 申请方式请参考上述调用规范下的认证方式。 |
| 其他非200值 | 业务异常 | 参考 errmsg 字段获取具体错误原因 |
错误响应示例:
{
"errcode": 401,
"errmsg": "authorized error"
}curl 示例
curl -X POST https://tool-gateway.linkfox.com/sif/asinKeywords \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"asin": "B0XXXXXXXX", "country": "US", "pageSize": 100, "sortBy": "estSearchesNum", "desc": true}'带关键词筛选和条件的示例
curl -X POST https://tool-gateway.linkfox.com/sif/asinKeywords \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"asin": "B0XXXXXXXX", "country": "US", "keyword": "charger", "conditions": "nfPosition,isPurchaseKw", "sortBy": "lastRank", "desc": false, "pageNum": 1, "pageSize": 50}'---
Feedback API
This endpoint is separate from the tool API above. Do not mix the two base URLs.
- POST
https://skill-api.linkfox.com/api/v1/public/feedback - Content-Type:
application/json
{
"skillName": "linkfox-xxx-xxx",
"sentiment": "POSITIVE",
"category": "OTHER",
"content": "Results were accurate, user was satisfied."
}Field rules:
skillName: Use this skill'snamefrom the YAML frontmattersentiment: Choose ONE —POSITIVE(praise),NEUTRAL(suggestion without emotion),NEGATIVE(complaint or error)category: Choose ONE —BUG(malfunction or wrong data),COMPLAINT(user dissatisfaction),SUGGESTION(improvement idea),OTHERcontent: Include what the user said or intended, what actually happened, and why it is a problem or praise
#!/usr/bin/env python3
"""
Skill response I/O helper — wraps any main script to persist large API
responses to disk, then offers a `read` subcommand to extract specific fields
from those persisted files. Generic, business-agnostic.
This script is bundled into each skill's scripts/ directory by tools/response_io/sync.py.
The agent must pass --script <path> to identify which main script to execute.
Usage:
python scripts/response_io.py run --script <PATH> --out-dir <DIR> '<json_params>' [--label NAME] [--timeout SEC]
python scripts/response_io.py read <file> (--path "<JMESPath>" | --fields "f1,f2,...") [--limit N] [--offset M] [--format json|jsonl|csv|table]
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 10):
sys.exit(
"Error: Python 3.10+ required (current: "
f"{sys.version_info.major}.{sys.version_info.minor}). "
"Please upgrade Python."
)
import argparse
import csv
import io
import json
import os
import re
import secrets
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any
# Force UTF-8 stdout/stderr so non-ASCII chars in previews and API responses
# print correctly on Windows (default cp936 / gbk).
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
try:
import jmespath # type: ignore
HAS_JMESPATH = True
except ImportError:
HAS_JMESPATH = False
MAX_STRING_LEN = 120
MAX_DEPTH = 3
SAMPLE_KEY_CAP = 15
RAW_TEXT_PEEK = 500
DEFAULT_TIMEOUT_SEC = 300
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _err(msg: str, code: int = 1) -> None:
print(msg, file=sys.stderr)
sys.exit(code)
def _resolve_script(script_arg: str) -> Path:
p = Path(script_arg).expanduser()
if not p.is_absolute():
# Resolve relative to the current working directory the agent invoked from.
p = (Path.cwd() / p).resolve()
else:
p = p.resolve()
if not p.is_file():
_err(f"--script path not found: {p}")
return p
def _resolve_skill_name(main_script: Path) -> str:
"""Best-effort skill name extraction for filename prefixing.
main_script lives at <skill_dir>/scripts/<name>.py — return <skill_dir>'s
folder name. Fall back to the script's stem if structure differs.
"""
try:
if main_script.parent.name == "scripts":
return main_script.parents[1].name
except IndexError:
pass
return main_script.stem
def _sanitize_label(label: str) -> str:
"""Allow only safe filename chars in --label to prevent path traversal."""
cleaned = re.sub(r"[^\w\-]", "_", label)
return cleaned[:64] # cap length
def _truncate_string(s: str) -> str:
if len(s) <= MAX_STRING_LEN:
return s
return s[:MAX_STRING_LEN] + f"...(truncated, total {len(s)} chars)"
def _truncate_value(value: Any, depth: int = 0) -> Any:
"""Recursively truncate strings, deep nesting, and large arrays for preview."""
if depth >= MAX_DEPTH:
if isinstance(value, dict):
return f"<truncated nested object, keys: {list(value.keys())[:10]}>"
if isinstance(value, list):
return f"<truncated nested array, length: {len(value)}>"
if isinstance(value, str):
return _truncate_string(value)
return value
if isinstance(value, str):
return _truncate_string(value)
if isinstance(value, dict):
out = {k: _truncate_value(v, depth + 1) for k, v in value.items()}
return out
if isinstance(value, list):
if not value:
return []
truncated = [_truncate_value(value[0], depth + 1)]
if len(value) > 1:
# Note total length on the parent — keep the array type-homogeneous
# so downstream consumers can iterate without special-casing strings.
truncated.append({"_omitted_items": len(value) - 1})
return truncated
return value
def _shape_of(value: Any, top: bool = False) -> Any:
"""Lightweight schema description for the preview block."""
if isinstance(value, dict):
keys = list(value.keys())
out: dict[str, Any] = {"type": "object", "top_keys" if top else "keys": keys}
if top:
for k in keys[:8]:
out[k] = _shape_of(value[k])
return out
if isinstance(value, list):
out = {"type": "array", "length": len(value)}
if value and isinstance(value[0], dict):
out["item_keys"] = list(value[0].keys())
elif value:
out["item_type"] = type(value[0]).__name__
return out
return {"type": type(value).__name__}
def _build_sample(value: Any) -> Any:
"""First-record sample with explicit truncation marker."""
if isinstance(value, list):
if not value:
return {"_truncated_record": True, "_note": "array is empty"}
first = value[0]
if isinstance(first, dict):
sample = {"_truncated_record": True, "_note": f"first of {len(value)} items"}
sample.update(_truncate_value(first, depth=1))
return sample
return {"_truncated_record": True, "_note": f"first of {len(value)} items", "value": _truncate_value(first, depth=1)}
if isinstance(value, dict):
sample = {"_truncated_record": True, "_note": "top-level object (truncated)"}
sample.update(_truncate_value(value, depth=1))
return sample
return {"_truncated_record": True, "value": _truncate_value(value, depth=1)}
def _shrink_preview(preview: dict) -> dict:
"""Cap the sample's value fields when it has many keys.
`shape.*.item_keys` is the single source of truth for the full key list
(always complete, no truncation). The sample only ever shows up to
SAMPLE_KEY_CAP fields with their concrete values, since the agent only
needs a feel for value shapes — for the full menu of available fields,
they read `shape`.
"""
sample = preview.get("sample")
if isinstance(sample, dict):
meta_keys = {"_truncated_record", "_note"}
data_keys = [k for k in sample.keys() if k not in meta_keys]
if len(data_keys) > SAMPLE_KEY_CAP:
kept = data_keys[:SAMPLE_KEY_CAP]
new_sample = {k: v for k, v in sample.items() if k in meta_keys or k in kept}
base_note = sample.get("_note", "")
extra = (
f"showing first {SAMPLE_KEY_CAP} of {len(data_keys)} fields "
f"(see `shape` for the complete key list)"
)
new_sample["_note"] = f"{base_note}; {extra}" if base_note else extra
preview["sample"] = new_sample
return preview
# ---------------------------------------------------------------------------
# `run` subcommand
# ---------------------------------------------------------------------------
def cmd_run(args: argparse.Namespace) -> int:
main_script = _resolve_script(args.script)
skill_name = _resolve_skill_name(main_script)
out_dir = Path(args.out_dir).expanduser().resolve()
try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
_err(f"Failed to create --out-dir {out_dir}: {e}")
if not os.access(out_dir, os.W_OK):
_err(f"--out-dir is not writable: {out_dir}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
rand = secrets.token_hex(3)
safe_label = _sanitize_label(args.label) if args.label else ""
label_part = f"__{safe_label}" if safe_label else ""
out_file = out_dir / f"{skill_name}__{timestamp}_{rand}{label_part}.json"
# Force the child process to emit UTF-8 regardless of the host console
# encoding (Windows defaults to cp936 / gbk and would otherwise corrupt
# non-ASCII bytes when we read them back).
child_env = os.environ.copy()
child_env["PYTHONIOENCODING"] = "utf-8"
timed_out = False
try:
proc = subprocess.run(
[sys.executable, str(main_script), args.params],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=child_env,
timeout=args.timeout,
)
stdout_text = proc.stdout or ""
stderr_text = proc.stderr or ""
returncode = proc.returncode
except subprocess.TimeoutExpired as e:
timed_out = True
stdout_text = (e.stdout.decode("utf-8", errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")) or ""
stderr_text = (e.stderr.decode("utf-8", errors="replace") if isinstance(e.stderr, bytes) else (e.stderr or "")) or ""
returncode = 124 # convention for timeout
# Always write the captured stdout to disk, even if not JSON.
try:
out_file.write_text(stdout_text, encoding="utf-8")
except OSError as e:
_err(f"Failed to write output file {out_file}: {e}")
if stderr_text:
sys.stderr.write(stderr_text)
# Try to parse the captured stdout as JSON for the preview.
try:
parsed = json.loads(stdout_text) if stdout_text.strip() else None
format_kind = "json"
except json.JSONDecodeError:
parsed = None
format_kind = "raw_text"
preview: dict[str, Any] = {
"_preview": {
"is_preview": True,
"warning": (
"PREVIEW ONLY — NOT FULL DATA. The full response is saved to `file`. "
"Use `python scripts/response_io.py read <file> --fields '...'` to extract "
"specific fields, or `--path '<JMESPath>'` for complex projections."
),
},
}
# Surface failures prominently so agents don't mistake a stub preview for success.
if returncode != 0 or timed_out:
stderr_snippet = stderr_text[-500:] if stderr_text else ""
preview["_error"] = {
"exit_code": returncode,
"timed_out": timed_out,
"stderr_snippet": stderr_snippet,
"hint": "The wrapped script failed or timed out. The output file may be empty or partial.",
}
preview.update({
"file": str(out_file),
"size_bytes": out_file.stat().st_size,
"skill": skill_name,
"exit_code": returncode,
"format": format_kind,
"label": safe_label or None,
"next_steps_hint": (
"use: python scripts/response_io.py read <file> --fields '...' | --path '...'"
),
})
if format_kind == "json":
preview["shape"] = _shape_of(parsed, top=True)
preview["sample"] = _build_sample(parsed)
else:
peek = stdout_text[:RAW_TEXT_PEEK]
preview["raw_text_peek"] = peek
preview["raw_text_total_chars"] = len(stdout_text)
preview["sample"] = {
"_truncated_record": True,
"_note": f"stdout was not valid JSON; first {RAW_TEXT_PEEK} chars shown above in raw_text_peek",
}
preview = _shrink_preview(preview)
print(json.dumps(preview, ensure_ascii=False, indent=2))
return returncode
# ---------------------------------------------------------------------------
# `read` subcommand
# ---------------------------------------------------------------------------
def _load_json(path: Path) -> Any:
try:
text = path.read_text(encoding="utf-8")
except OSError as e:
_err(f"Failed to read file {path}: {e}")
try:
return json.loads(text)
except json.JSONDecodeError as e:
_err(f"File is not valid JSON: {path}\n{e}")
def _basic_dot_path(data: Any, path: str) -> Any:
"""Pure-stdlib dot-path resolver. No [*] support — callers fall back here only when jmespath is unavailable AND the path has no [*]."""
cur = data
for part in path.split("."):
if isinstance(cur, dict):
cur = cur.get(part)
else:
return None
return cur
def _resolve_field(data: Any, expr: str) -> Any:
if HAS_JMESPATH:
return jmespath.search(expr, data)
if "[" in expr or "*" in expr:
_err(
f"jmespath is required for expression '{expr}'. "
f"Install with: pip install jmespath"
)
return _basic_dot_path(data, expr)
def _project_fields(data: Any, fields: list[str]) -> Any:
"""Run each field expr; if any returns a list, zip them into list-of-dicts."""
resolved: dict[str, Any] = {f: _resolve_field(data, f) for f in fields}
list_lengths = [len(v) for v in resolved.values() if isinstance(v, list)]
if not list_lengths:
return resolved
# All list values must be same length to zip cleanly.
if len(set(list_lengths)) > 1:
# Fallback: return the dict as-is so caller can inspect mismatches.
return resolved
n = list_lengths[0]
rows = []
for i in range(n):
row = {}
for f, v in resolved.items():
row[f] = v[i] if isinstance(v, list) else v
rows.append(row)
return rows
def _apply_slice(value: Any, limit: int | None, offset: int | None) -> Any:
if not isinstance(value, list):
return value
start = offset or 0
end = (start + limit) if limit is not None else None
return value[start:end]
def _format_output(value: Any, fmt: str) -> str:
if fmt == "json":
return json.dumps(value, ensure_ascii=False, indent=2)
if fmt == "jsonl":
if isinstance(value, list):
return "\n".join(json.dumps(item, ensure_ascii=False) for item in value)
return json.dumps(value, ensure_ascii=False)
if fmt in ("csv", "table"):
if not isinstance(value, list) or not value:
_err(f"--format {fmt} requires a non-empty list result")
if not all(isinstance(item, dict) for item in value):
_err(f"--format {fmt} requires list-of-objects, got list of {type(value[0]).__name__}")
keys: list[str] = []
for item in value:
for k in item.keys():
if k not in keys:
keys.append(k)
if fmt == "csv":
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=keys, extrasaction="ignore")
writer.writeheader()
for item in value:
writer.writerow({k: _stringify(item.get(k)) for k in keys})
return buf.getvalue().rstrip("\n")
# table: simple aligned columns
rows = [[_stringify(item.get(k)) for k in keys] for item in value]
widths = [len(k) for k in keys]
for row in rows:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(cell))
lines = [
" ".join(k.ljust(widths[i]) for i, k in enumerate(keys)),
" ".join("-" * widths[i] for i in range(len(keys))),
]
for row in rows:
lines.append(" ".join(row[i].ljust(widths[i]) for i in range(len(keys))))
return "\n".join(lines)
_err(f"Unknown --format: {fmt}")
return "" # unreachable
def _stringify(v: Any) -> str:
if v is None:
return ""
if isinstance(v, (dict, list)):
return json.dumps(v, ensure_ascii=False)
return str(v)
def cmd_read(args: argparse.Namespace) -> int:
if not args.path and not args.fields:
_err("read: either --path or --fields is required")
if args.path and args.fields:
_err("read: --path and --fields are mutually exclusive")
file_path = Path(args.file).expanduser().resolve()
data = _load_json(file_path)
if args.path:
result = _resolve_field(data, args.path)
else:
fields = [f.strip() for f in args.fields.split(",") if f.strip()]
if not fields:
_err("--fields parsed to empty list")
result = _project_fields(data, fields)
result = _apply_slice(result, args.limit, args.offset)
print(_format_output(result, args.format))
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
prog="response_io.py",
description="Persist large skill API responses to disk and read fields on demand.",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_run = sub.add_parser(
"run",
help="Execute a main script and persist its stdout to a file; "
"print only a lightweight preview to stdout.",
)
p_run.add_argument("params", help="JSON params string passed verbatim to the main script (argv[1]).")
p_run.add_argument("--script", required=True, help="Path to the main script to execute, e.g. scripts/my_api.py")
p_run.add_argument("--out-dir", required=True, help="Directory to write the response file into (created if missing).")
p_run.add_argument("--label", default=None, help="Optional filename suffix; sanitized to safe filename characters.")
p_run.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SEC, help=f"Subprocess timeout in seconds (default: {DEFAULT_TIMEOUT_SEC}).")
p_run.set_defaults(func=cmd_run)
p_read = sub.add_parser(
"read",
help="Extract specific fields from a previously persisted response file.",
)
p_read.add_argument("file", help="Path to the persisted JSON response file.")
g = p_read.add_mutually_exclusive_group()
g.add_argument("--path", default=None, help="JMESPath expression, e.g. 'data[*].{asin: asin, title: title}'.")
g.add_argument("--fields", default=None, help="Comma-separated field paths, e.g. 'data[*].asin,data[*].title'.")
p_read.add_argument("--limit", type=int, default=None, help="Take at most N items (when result is a list).")
p_read.add_argument("--offset", type=int, default=None, help="Skip the first M items (when result is a list).")
p_read.add_argument("--format", choices=["json", "jsonl", "csv", "table"], default="json", help="Output format (default: json).")
p_read.set_defaults(func=cmd_read)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
SIF ASIN Keywords Query - LinkFox Skill
Calls the sif/asinKeywords API endpoint to retrieve traffic keywords for a given ASIN.
Usage:
python sif_asin_keywords.py '{"asin": "B0XXXXXXXX", "country": "US"}'
python sif_asin_keywords.py '{"asin": "B0XXXXXXXX", "country": "JP", "keyword": "charger", "conditions": "nfPosition", "sortBy": "estSearchesNum", "desc": true}'
"""
import json
import os
import sys
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
API_URL = "https://tool-gateway.linkfox.com/sif/asinKeywords"
# Valid marketplace codes
VALID_COUNTRIES = {
"US", "CA", "MX", "UK", "DE", "FR", "IT", "ES",
"JP", "IN", "AU", "BR", "NL", "SE", "PL", "TR",
"AE", "SA", "SG",
}
# Valid condition filter values
VALID_CONDITIONS = {
"nfPosition", "isSpAd", "isBrandAd", "isVedioAd",
"isAC", "isER", "isTr", "isMainKw", "isAccurateKw",
"isAccurateAboveKw", "isAccurateTailKw", "isPurchaseKw",
"isQualityKw", "isStableKw", "isLossKw", "isInvalidKw",
}
# Valid sort field values
VALID_SORT_FIELDS = {"lastRank", "adLastRank", "updateTime", "searchesRank", "estSearchesNum", ""}
def get_api_key():
"""Retrieve the API key from environment, with a friendly prompt if missing."""
key = os.environ.get("LINKFOXAGENT_API_KEY")
if not key:
print(
"API Key not configured. Please complete authorization first:\n"
"1. Visit https://skill.linkfox.com/linkfoxskills/guide.htm to obtain your Key\n"
"2. Set the environment variable: export LINKFOXAGENT_API_KEY=your-key-here",
file=sys.stderr,
)
sys.exit(1)
return key
def validate_params(params: dict):
"""Validate request parameters before sending to the API."""
# asin is required
if "asin" not in params or not params["asin"]:
print("Error: 'asin' is a required parameter.", file=sys.stderr)
sys.exit(1)
# Validate country code if provided
country = params.get("country", "US")
if country not in VALID_COUNTRIES:
print(
f"Error: Invalid country code '{country}'. "
f"Valid values: {', '.join(sorted(VALID_COUNTRIES))}",
file=sys.stderr,
)
sys.exit(1)
# Validate conditions if provided
if "conditions" in params and params["conditions"]:
for cond in params["conditions"].split(","):
cond = cond.strip()
if cond not in VALID_CONDITIONS:
print(
f"Error: Invalid condition '{cond}'. "
f"Valid values: {', '.join(sorted(VALID_CONDITIONS))}",
file=sys.stderr,
)
sys.exit(1)
# Validate sortBy if provided
if "sortBy" in params and params["sortBy"] not in VALID_SORT_FIELDS:
print(
f"Error: Invalid sortBy value '{params['sortBy']}'. "
f"Valid values: {', '.join(f for f in sorted(VALID_SORT_FIELDS) if f)}",
file=sys.stderr,
)
sys.exit(1)
# Validate pageSize range if provided
if "pageSize" in params:
ps = params["pageSize"]
if not isinstance(ps, int) or ps < 10 or ps > 100:
print("Error: pageSize must be an integer between 10 and 100.", file=sys.stderr)
sys.exit(1)
def call_api(params: dict) -> dict:
"""Call the SIF ASIN Keywords API endpoint."""
api_key = get_api_key()
data = json.dumps(params).encode("utf-8")
req = Request(
API_URL,
data=data,
headers={
"Authorization": api_key,
"Content-Type": "application/json",
"User-Agent": "LinkFox-Skill/1.0",
},
method="POST",
)
try:
with urlopen(req, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as e:
body = e.read().decode("utf-8") if e.fp else ""
return {"error": f"HTTP {e.code}: {e.reason}", "details": body}
except URLError as e:
return {"error": f"Connection failed: {e.reason}"}
def main():
if len(sys.argv) < 2:
print("Usage: sif_asin_keywords.py '<JSON parameters>'", file=sys.stderr)
print(
"Example: sif_asin_keywords.py "
"'{\"asin\": \"B0XXXXXXXX\", \"country\": \"US\"}'",
file=sys.stderr,
)
print(
"\nRequired parameter:\n"
" asin - Amazon ASIN to query\n"
"\nOptional parameters:\n"
" country - Marketplace code (default: US)\n"
" keyword - Keyword filter text\n"
" conditions - Comma-separated condition filters\n"
" sortBy - Sort field (lastRank, adLastRank, updateTime, searchesRank, estSearchesNum)\n"
" desc - Descending order (default: true)\n"
" pageNum - Page number (default: 1)\n"
" pageSize - Results per page, 10-100 (default: 100)",
file=sys.stderr,
)
sys.exit(1)
try:
params = json.loads(sys.argv[1])
except json.JSONDecodeError as e:
print(f"Invalid parameter format: {e}", file=sys.stderr)
sys.exit(1)
validate_params(params)
result = call_api(params)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()