@kody/producthunt
README.md
222 lines · 8.6 KB · Markdown@kody/producthunt
Intent
Give any Kody account reusable Product Hunt helpers for today’s posts,
lookups (posts, hunters, topics, collections), and comments as allowed by
the Product Hunt API. The public GraphQL schema can read comments; it does
not expose a comment-create mutation. Writes that are in the schema (goals
and follow/unfollow) go through ./request with dryRun: true / confirm: true.
This listing is meant to be forked. After you fork, connect your Product
Hunt app or developer token and run ./smoke-test on your copy. Do not
treat the live @kody/producthunt integration as yours.
Share this package as https://kody.codes/@kody/producthunt
(never a /community/{listing_id} URL).
When To Use
- List today’s featured hunts without writing GraphQL by hand
- Look up a post, hunter, topic, or collection by id or slug
- Read comments on a post (
./list-comments) - Call an unwrapped Product Hunt GraphQL operation through
./request - Verify credentials with
./smoke-testbefore building workflows
Do not use this package as a scraper, a place to paste access tokens, or a comment poster — Product Hunt’s public API does not expose comment writes.
Agent setup
Product Hunt has no built-in Kody OAuth app. Tokens come from a bring-your-own confidential OAuth app or a developer token saved as a user secret. Never paste a client secret or access token into chat.
Required API host: api.producthunt.com. Approve it in the account
secrets UI. Saving a secret does not approve hosts by itself.
| Lane | When to use | Credential |
|---|---|---|
| OAuth (recommended) | Connected hunter (viewer), private fields, refreshable tokens | Saved integration producthunt or producthunt-<purpose> |
| Developer token | Public reads when you only have a dashboard token | User secret producthuntAccessToken (or producthuntAccessToken-<purpose>) |
Lane A — BYO OAuth
- Create a Confidential OAuth app at Product Hunt API applications. Public clients cannot store the secret Kody needs.
- Register the redirect URI exactly as
https://kody.codes/connect/oauth. - Request scopes
publicandprivate(space-separated). Addwriteonly when Product Hunt has approved your app for mutations. - Open this prefilled connect URL while signed in to Kody:
- Paste the Client ID and Client Secret into the Kody form (not into chat). If you need to save the client secret yourself first:
- Approve host
api.producthunt.com. - Reconnect later with https://kody.codes/connect/oauth?provider=producthunt
Decoded connect fields:
- Redirect / callback:
https://kody.codes/connect/oauth - Authorize:
https://api.producthunt.com/v2/oauth/authorize - Token:
https://api.producthunt.com/v2/oauth/token - API:
https://api.producthunt.com/v2/api/graphql - Flow:
confidential - Host:
api.producthunt.com - Scopes:
public,private
To connect a second account, change provider (for example
provider=producthunt-work) and pass account: 'work' or
integrationName: 'producthunt-work' on every call.
Lane B — Developer / access token
- Create or copy a developer token from the same API applications page.
- Save it (do not paste the value in chat):
- Approve host
api.producthunt.com. - Run
./smoke-test. For a second token useproducthuntAccessToken-workand passsecretName: 'producthuntAccessToken-work'(oraccount: 'work').
Scopes
| Scope | Needed for |
|---|---|
public | Featured posts, lookups, comments, topics, collections |
private | ./get-viewer (viewer.user) |
write | Goal and follow mutations via ./request (Product Hunt must approve the app) |
./request uses whatever scopes the saved token already has.
Auth / transport
There is no Product Hunt OpenAPI surface. This package posts GraphQL to
https://api.producthunt.com/v2/api/graphql with either
createAuthenticatedFetch('producthunt') or
Authorization: Bearer from producthuntAccessToken.
Mutation safety
The public schema’s mutations are goals and user follow/unfollow — not
comments. ./request treats query documents as read-only. Mutation documents
need confirm: true. Pass dryRun: true to inspect the payload without
calling Product Hunt.
import request from 'kody:@kody/producthunt/request'
export default async function main() {
return await request({
query: `mutation FollowUser($userId: ID!) { userFollow(input: { userId: $userId }) { user { id } } }`,
variables: { userId: 'user-id-from-caller' },
dryRun: true,
})
}Multiple accounts
Every export accepts:
account—work→ integrationproducthunt-work, secretproducthuntAccessToken-workintegrationName/integration— saved OAuth name (producthuntorproducthunt-*)secretName— developer-token secret override (defaultproducthuntAccessToken)auth—'oauth'or'token'when both lanes exist
Do not hard-code a hunter username or post slug.
Exports
| Export | Purpose |
|---|---|
. | Package overview and setup URLs. Pass { action: 'smoke-test' } to smoke. |
./accounts | Resolve integration/secret names and report what is connected |
./smoke-test | Setup URLs always; live featured/today sample when credentials exist |
./guide | Connect URL and setup notes ({ origin?: string }) |
./list-today-posts | Featured hunts for the current Product Hunt day (Pacific Time) |
./list-posts | Paginated posts (featured, date window, topic, order) |
./get-post | Post by id or slug |
./list-comments | Comments on a post (read-only; API has no comment create) |
./get-user | Hunter by id or username |
./get-viewer | Connected hunter (private scope) |
./get-topic / ./list-topics | Topic lookup and search |
./get-collection / ./list-collections | Collection lookup and lists |
./request | Authenticated GraphQL escape hatch (dryRun / confirm on mutations) |
./types | Shared TypeScript type catalog |
Page sizes are clamped to 1–20 to match the Product Hunt API.
Smoke test
After connect or reconnect, invoke ./smoke-test in the forked package
runtime (not a static import):
import { packages } from 'kody:runtime'
export default async function main() {
return await packages.invoke({
kodyId: 'producthunt',
exportName: './smoke-test',
})
}Or through the root export:
import producthunt from 'kody:@kody/producthunt'
export default async function main() {
return await producthunt({ action: 'smoke-test' })
}Without credentials this returns { ok: true, live: false } plus the connect
and token setup URLs. After OAuth or a developer token is saved it reads
today’s featured hunts and returns { live: true, hasViewer, todayPostCount }
without hunter PII.
Examples
import listTodayPosts from 'kody:@kody/producthunt/list-today-posts'
export default async function main() {
return await listTodayPosts({ first: 5 })
}import getPost from 'kody:@kody/producthunt/get-post'
import listComments from 'kody:@kody/producthunt/list-comments'
export default async function main() {
const post = await getPost({ slug: 'some-product-slug' })
const comments = await listComments({ slug: post.slug, first: 10 })
return { post, comments }
}Troubleshooting
- Connect page asks for authorize/token URLs: use the complete first-time URL in Lane A.
- HTTP 401 / expired token: reconnect at https://kody.codes/connect/oauth?provider=producthunt
viewer.userempty: reconnect with theprivatescope.- Mutation
FORBIDDEN: Product Hunt must approvewriteon your app, then reconnect with that scope. redirect_urimismatch: the Product Hunt app redirect must be exactlyhttps://kody.codes/connect/oauth.