GrinGhost est un provider OIDC custom + intermédiaire de paiement. Tu listes tes produits dans le dashboard, GrinGhost valide chaque action et gère les crédits via Stripe Connect.
GrinGhost est un provider OIDC complet. Ajoute-le dans Supabase comme custom provider — il auto-découvre tous les endpoints via son document de configuration.
1. Créer un Agent IA dans le dashboard GrinGhost
Dashboard → Espace développeur → Mes Agents IA → Nouvel Agent. Renseigne le nom et l'URL de redirect. Tu obtiens deux sets de clés :
Live — clé API + OAuth Secret : production
Sandbox — clé API sandbox + OAuth Secret sandbox : développement (crédits fictifs)
Les clés sont affichées une seule fois à la création. Si tu les perds, utilise "Regénérer les clés" dans le dashboard (les anciennes sont invalidées immédiatement).
URL de redirect — GrinGhost valide que le redirect_uri passé lors du flux OAuth a le même origin que l'URL enregistrée dans le dashboard. Par exemple, si tu enregistres https://myapp.com, tu peux passer https://myapp.com/auth/callback mais pas https://other.com/callback. En sandbox, enregistre http://localhost:3000 pour le développement local.
2. Créer ton catalogue de produits
Avant d'appeler l'API, tu dois enregistrer tes produits dans le dashboard : Dashboard → Mes Agents IA → ton Agent IA → Catalogue.
Chaque produit a un identifiant unique, un nom lisible, et un prix en crédits. GrinGhost n'autorisera aucun débit sur un produit non enregistré.
text
Exemple de catalogue :
analyze_image → 5 crédits (Analyser une image)
generate_text → 3 crédits (Générer du texte)
summarize → 2 crédits (Résumer un document)
Tu ne peux pas modifier les prix d'un produit dans les 30 jours suivant un achat de crédits sur ton site. Ce verrou protège tes utilisateurs contre toute dévaluation après achat.
3. Ajouter GrinGhost dans Supabase
Authentication → Sign In / Up → Social Providers → Add provider → Custom OAuth 2.0.
Champ
Dev (sandbox)
Prod (live)
Provider name
gringhost
gringhost
Issuer URL
https://gringhost.com
https://gringhost.com
Client ID
sandbox_api_key
api_key
Client Secret
sandbox_oauth_secret
oauth_secret
Supabase auto-découvre les endpoints via https://gringhost.com/.well-known/openid-configuration. En dev, utilise les clés sandbox — tu ne changes que les clés pour passer en prod.
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(origin + '/dashboard')
}
6. Lire l'identifiant GrinGhost
L'ID GrinGhost est dans l'identity custom:gringhost — pas dans app_metadata.provider_id. Utilise ce helper :
ts
// lib/gringhost.ts
import type { User } from '@supabase/supabase-js'
export function getGringhostUserId(user: User): string | null {
return user.identities?.find(i => i.provider === 'custom:gringhost')?.id ?? null
}
// Dans une route API
const { data: { user } } = await supabase.auth.getUser()
const gringhostId = getGringhostUserId(user)
// ↑ UUID GrinGhost — différent de user.id (UUID de TON projet Supabase)
Ne jamais utiliser user.id pour appeler l'API GrinGhost. Toujours passer par getGringhostUserId(user).
7. Débiter des crédits
Le débit nécessite un token d'action — un jeton usage unique généré par le navigateur de l'utilisateur au moment où il déclenche l'action. Ce token prouve le consentement explicite et empêche tout débit en background côté serveur.
Étape 1 — le browser génère un token d'action (appel direct vers GrinGhost) :
Le accessToken GrinGhost est retourné par Supabase après le OAuth exchange — disponible via session.provider_token ou en le stockant au moment du callback.
Étape 2 — le browser envoie le token à ton backend, puis ton backend appelle /api/v1/deduct :
ts
// Côté client — après avoir obtenu l'action_token
const actionToken = await getActionToken(accessToken, APP_ID, 'analyze_image')
const res = await fetch('/api/analyze', {
method: 'POST',
body: JSON.stringify({ imageUrl, action_token: actionToken }),
})
// Côté serveur (ta route /api/analyze)
const res = await fetch('https://gringhost.com/api/v1/deduct', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.GRINGHOST_API_KEY,
},
body: JSON.stringify({
user_id: gringhostId, // getGringhostUserId(user)
product_id: 'analyze_image',
action_token: actionToken, // reçu du browser
}),
})
const data = await res.json()
// data.remaining_credits = solde mis à jour
Le action_token est obligatoire dès que tu fournis un product_id. Sans lui, GrinGhost retourne action_token_required (400). Il expire en 2 minutes et ne peut être utilisé qu'une seule fois.
8. Lire le solde de crédits
ts
const res = await fetch(`https://gringhost.com/api/v1/credits?user_id=${gringhostId}`, {
headers: { 'x-api-key': process.env.GRINGHOST_API_KEY },
})
const { credits, expires_at } = await res.json()
// credits = solde disponible sur ton app
// expires_at = date d'expiration (null si aucun achat)
9. Recharger des crédits
Quand un utilisateur manque de crédits (402), redirige-le vers la page d'achat GrinGhost en passant l'app_id — sans lui, la page affiche uniquement un message d'orientation :
ts
const APP_ID = process.env.NEXT_PUBLIC_GRINGHOST_APP_ID // UUID de ton app
if (res.status === 402) {
window.open(`https://gringhost.com/buy?app=${APP_ID}`, '_blank')
}
Conseil à communiquer à tes utilisateurs
Achète uniquement les crédits dont tu as besoin dans les 12 prochains mois. Les crédits non utilisés expirent, et acheter plus que nécessaire revient à exposer inutilement son argent. Consulter les tarifs →
Le catalogue est la liste de tes produits IA avec leur prix en crédits. GrinGhost refuse tout débit sur un product_id absent de ce catalogue. C'est le mécanisme central de validation.
Règles du catalogue
Chaque produit a un product_id unique (slug sans espaces), un nom lisible, et un prix en crédits (entier positif)
Un produit peut être désactivé mais pas supprimé s'il a été utilisé
Les prix ne peuvent pas être modifiés dans les 30 jours suivant un achat de crédits sur ton site
Tu peux ajouter de nouveaux produits à tout moment
Afficher le prix avant débit
Tu es tenu d'afficher le coût en crédits de chaque action sur ton site, de façon visible avant que l'utilisateur la déclenche. Le non-respect peut entraîner la suspension de l'accès à l'API.
tsx
// Exemple d'affichage avant action
<button onClick={handleAnalyze}>
Analyser l'image — <span style={{ color: '#fbbf24' }}>5 crédits</span>
</button>
Un token d'action est un jeton usage unique créé par GrinGhost quand l'utilisateur déclenche une action depuis son navigateur. Il prouve que le débit correspond à une intention explicite — GrinGhost refuse tout appel à /api/v1/deduct sans ce token.
Pourquoi ce mécanisme ?
Empêche le dev de débiter en background sans action utilisateur
Le prix est verrouillé dans le token à la création — impossible de le modifier entre la génération et le débit
Usage unique + TTL 2 min — pas de replay possible
Le token est lié à un triplet (user_id, app_id, product_id) — non transférable
Rate limit : 10 tokens maximum par utilisateur et par Agent IA sur une fenêtre glissante de 60 secondes
POST /api/v1/token
Valeur
URL
https://gringhost.com/api/v1/token
Auth
Authorization: Bearer <access_token>
Appelé depuis
Navigateur du client (pas ton backend)
Paramètre body
Type
Description
app_id
string
UUID de ton application GrinGhost
product_id
string
Identifiant du produit à débiter
json
// Succès (200)
{
"action_token": "a3f8b2...", // 64 chars hex, usage unique
"expires_at": "2026-06-12T10:32:00.000Z",
"credit_cost": 5
}
// Token GrinGhost invalide ou expiré (401)
{ "error": "invalid_token" }
// app_id ne correspond pas au token (403)
{ "error": "app_mismatch" }
// Utilisateur n'a pas autorisé l'app (403)
{ "error": "not_authorized" }
// product_id absent du catalogue (400)
{ "error": "product_not_found" }
// Trop de tokens générés — attendre avant de réessayer (429)
// Header: Retry-After: 60
{ "error": "rate_limited", "message": "Trop de tokens générés. Limite : 10 par minute." }
Récupérer l'access_token GrinGhost
L'access_token est disponible dans la session Supabase juste après le OAuth exchange. Stocke-le côté client pour le réutiliser jusqu'à expiration.
ts
// app/auth/callback/route.ts — stocker le token
const { data } = await supabase.auth.exchangeCodeForSession(code)
const accessToken = data.session?.provider_token // GrinGhost access_token
// Ou depuis le client après auth
const { data: { session } } = await supabase.auth.getSession()
const accessToken = session?.provider_token
Utilisés automatiquement par Supabase via auto-discovery.
Endpoint
Rôle
GET /.well-known/openid-configuration
Document de découverte OIDC
GET /api/oauth/jwks
Clés publiques RSA pour valider les id_token
GET /oauth/authorize
Page de consentement utilisateur
POST /api/oauth/token
Échange le code contre access_token + id_token
GET /api/oauth/userinfo
Retourne sub, email, name
POST /api/v1/token
Appelé depuis le navigateur du client. Auth par GrinGhost access_token (Bearer).
Paramètre
Type
Description
app_id
string
UUID de ton app
product_id
string
Produit à débiter
POST /api/v1/deduct
Appelé depuis ton backend. Auth par x-api-key.
Paramètre
Type
Description
user_id
string
ID GrinGhost — getGringhostUserId(user)
product_id
string
Produit à débiter (catalogue)
action_token
string
Token généré par le browser — obligatoire avec product_id
amount
number
Crédits bruts — rétrocompat, sans product_id uniquement
idempotency_key
string
Clé idempotence optionnelle
json
// Succès (200)
{ "success": true, "remaining_credits": 490 }
// action_token manquant (400)
{ "error": "action_token_required" }
// Token invalide, expiré ou déjà utilisé (403)
{ "error": "invalid_action_token" }
// Crédits insuffisants (402)
{ "error": "insufficient_credits", "remaining_credits": 3 }
// Produit absent du catalogue (400)
{ "error": "product_not_found" }
// Utilisateur n'a pas autorisé l'app (403)
{ "error": "user_not_authorized" }
// Limite quotidienne atteinte (429)
{ "error": "daily_limit_exceeded" }
// Clé API invalide (401)
{ "error": "unauthorized" }
GET /api/v1/credits
json
// GET /api/v1/credits?user_id=xxx
// Header: x-api-key: ...
{
"credits": 50000,
"expires_at": "2027-06-12T14:32:00.000Z"
}
// credits = solde disponible sur ton app
// expires_at = null si l'utilisateur n'a pas encore acheté de crédits
Tu fixes le prix de chaque produit dans le catalogue. Les crédits sont site-spécifiques — l'utilisateur achète des crédits sur ton site, pas sur gringhost.com.
Tes gains : ~0.000085 $ par crédit consommé (85% de ce que l'utilisateur a payé — GrinGhost prend 15% au moment de l'achat).
ts
// Calculer le prix d'une action
// 1 crédit = 0.0001 $ payé par l'utilisateur → tu reçois ~0.000085 $
const coutReel = 0.00005 // $ — ce que tu paies à l'API IA (ex: GPT-4o-mini court)
const marge = 2 // ×2 par rapport à ton coût
const credits = Math.ceil(coutReel * marge / 0.0001) // = 1 (minimum 1 entier)
Action
Coût API
×2
Crédits
Tu reçois
GPT-4o-mini court (~300 tok)
~$0.00005
$0.0001
1 cr
~$0.0001
GPT-4o-mini moyen (~1k tok)
~$0.0002
$0.0004
4 cr
~$0.0004
GPT-4o moyen (~2k tok)
~$0.005
$0.01
100 cr
~$0.01
DALL-E 3
~$0.04
$0.08
800 cr
~$0.08
Verrou tarifaire — Tu ne peux pas modifier les prix de ton catalogue dans les 30 jours suivant un achat de crédits sur ton site. Ce délai est imposé par GrinGhost pour protéger les utilisateurs contre toute dévaluation après achat.
Tu reçois 85% du prix de chaque pack acheté sur ton site. Les paiements passent par Stripe Connect — l'argent arrive directement sur ton compte Stripe, GrinGhost ne détient jamais les fonds.
Pack acheté
Prix
GrinGhost (15%)
Tu reçois
Starter
$5.00
$0.75
$4.25
Pro
$20.00
$3.00
$17.00
Max
$50.00
$7.50
$42.50
Les montants ci-dessus sont avant frais Stripe. Stripe prélève ~2.9% + $0.30 sur chaque achat (à la charge de GrinGhost, pas toi). Stripe prélève aussi de petits frais de virement sur tes retraits vers ta banque (à ta charge, variable selon ton pays).
Frais
Qui paie
Montant
Traitement paiement (achat pack)
GrinGhost
~2.9% + $0.30 / achat
Virement vers ta banque (payout)
Toi (dev)
Variable selon pays (~0.25%)
Pour recevoir les virements, connecte Stripe depuis Dashboard → Mes Agents IA → Connecter Stripe (identité + IBAN, ~5 min). Le minimum de retrait est de 50 000 crédits (~$4.25).
Le starter contient un fichier CLAUDE.md que Claude Code lit au démarrage. Il connaît déjà GrinGhost — tu décris ton produit, il construit.
Ce que Claude Code sait déjà
Le pattern exact pour toute route IA payante (product_id, getGringhostUserId, gestion 402)
Quels fichiers ne pas toucher (auth, session, crédits)
Comment récupérer l'utilisateur connecté côté serveur et client
La différence sandbox / live et les variables d'env
Démarrer
bash
npm install -g @anthropic-ai/claude-code
cd gringhost-nextjs-starter
claude
Exemples de prompts
text
"Ajoute un assistant de rédaction SEO. L'utilisateur entre un sujet,
reçoit 5 titres. Coût : 3 crédits (product_id: generate_titles)."
"Crée une page /summarize. L'utilisateur colle un texte, clique Résumer.
Coût : 2 crédits (product_id: summarize)."
Claude Code ne modifiera pas l'auth ni la logique GrinGhost existante. Voir le starter →