Guide développeur

Intégrer GrinGhost

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.

StarterRecommandé pour démarrer

gringhost-nextjs-starter — Next.js 16 + GrinGhost + Claude Code

Auth, catalogue produits, crédits et sandbox préconfigurés. Clone, configure 4 variables d'env, demande à Claude Code de construire ton service.

Cas 1 — Avec Supabase (recommandé)#

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.

ChampDev (sandbox)Prod (live)
Provider namegringhostgringhost
Issuer URLhttps://gringhost.comhttps://gringhost.com
Client IDsandbox_api_keyapi_key
Client Secretsandbox_oauth_secretoauth_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.

4. Ajouter le bouton de connexion

ts
import { createClient } from '@/lib/supabase/client'

const supabase = createClient()

await supabase.auth.signInWithOAuth({
  provider: 'custom:gringhost',
  options: {
    redirectTo: window.location.origin + '/auth/callback',
  },
})

5. Callback

ts
// 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) :

ts
// Côté client — déclenché au clic du bouton
async function getActionToken(accessToken: string, appId: string, productId: string) {
  const res = await fetch('https://gringhost.com/api/v1/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,   // GrinGhost access_token
    },
    body: JSON.stringify({ app_id: appId, product_id: productId }),
  })
  const { action_token } = await res.json()
  return action_token  // string hex 64 chars, TTL 2 min, usage unique
}
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 →

Catalogue de produits#

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>

Token d'action#

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
URLhttps://gringhost.com/api/v1/token
AuthAuthorization: Bearer <access_token>
Appelé depuisNavigateur du client (pas ton backend)
Paramètre bodyTypeDescription
app_idstringUUID de ton application GrinGhost
product_idstringIdentifiant 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

Sandbox — tester sans crédits réels#

En sandbox, /api/v1/deduct et /api/v1/credits retournent toujours 9999 crédits fictifs. La validation du catalogue reste active.

SandboxLive
Crédits débitésNon — fictifs (9999)Oui
Validation catalogueOuiOui
action_token requisNonOui
Flux OAuthIdentiqueIdentique
Badge page auth"MODE SANDBOX"Normal
Variable GRINGHOST_API_KEYsandbox_api_keyapi_key

Variables d'environnement

bash
# .env.local
GRINGHOST_BASE_URL=https://gringhost.com
GRINGHOST_API_KEY=<sandbox_api_key>

Passer en production

  1. Vercel → Environment Variables : remplacer GRINGHOST_API_KEY par la clé live
  2. Supabase → Providers → gringhost : remplacer Client ID et Client Secret par les clés live

Cas 2 — Autre framework#

À venir

Pour les stacks sans Supabase (Laravel, Express, Django…). Flow basé sur décodage direct du JWT. En attendant, utilise le Cas 1 avec Supabase.

Référence API#

Endpoints OIDC

Utilisés automatiquement par Supabase via auto-discovery.

EndpointRôle
GET /.well-known/openid-configurationDocument de découverte OIDC
GET /api/oauth/jwksClés publiques RSA pour valider les id_token
GET /oauth/authorizePage de consentement utilisateur
POST /api/oauth/tokenÉchange le code contre access_token + id_token
GET /api/oauth/userinfoRetourne sub, email, name

POST /api/v1/token

Appelé depuis le navigateur du client. Auth par GrinGhost access_token (Bearer).

ParamètreTypeDescription
app_idstringUUID de ton app
product_idstringProduit à débiter

POST /api/v1/deduct

Appelé depuis ton backend. Auth par x-api-key.

ParamètreTypeDescription
user_idstringID GrinGhost — getGringhostUserId(user)
product_idstringProduit à débiter (catalogue)
action_tokenstringToken généré par le browser — obligatoire avec product_id
amountnumberCrédits bruts — rétrocompat, sans product_id uniquement
idempotency_keystringClé 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

Tarifs & verrou tarifaire#

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)
ActionCoût API×2CréditsTu reçois
GPT-4o-mini court (~300 tok)~$0.00005$0.00011 cr~$0.0001
GPT-4o-mini moyen (~1k tok)~$0.0002$0.00044 cr~$0.0004
GPT-4o moyen (~2k tok)~$0.005$0.01100 cr~$0.01
DALL-E 3~$0.04$0.08800 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.

Gains & virements#

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éPrixGrinGhost (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).
FraisQui paieMontant
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).

Sécurité#

Ne jamais exposer ta clé API côté client. Le header x-api-key donne accès à /api/v1/deduct. Toujours appeler depuis ton backend.
ts
// Erreurs de /api/v1/token (appelé depuis le browser)
if (tokenRes.status === 429) {
  // Rate limit — 10 tokens/min dépassés
  // Header Retry-After: 60 → attendre avant de réessayer
  showError('Trop de requêtes — réessaie dans quelques secondes')
}
if (tokenRes.status === 403 && tokenData.error === 'not_authorized') {
  // L'utilisateur n'a plus l'app autorisée → relancer le flux OAuth
}

// Erreurs de /api/v1/deduct (appelé depuis ton backend)
if (res.status === 402) {
  // Crédits insuffisants → rediriger vers la page d'achat (app_id obligatoire)
  window.open(`https://gringhost.com/buy?app=${APP_ID}`, '_blank')
}
if (res.status === 400 && data.error === 'product_not_found') {
  // product_id absent du catalogue → vérifier le dashboard
}
if (res.status === 400 && data.error === 'action_token_required') {
  // action_token non fourni → toujours appeler /api/v1/token avant deduct
}
if (res.status === 403 && data.error === 'invalid_action_token') {
  // token expiré (>2 min) ou déjà utilisé → régénérer un nouveau token
  const newToken = await getActionToken(accessToken, APP_ID, productId)
}
if (res.status === 429) {
  showError('Limite quotidienne atteinte — réessaie demain')
}

Builder avec Claude Code#

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 →