Guide développeur

Intégrer GrinGhost

GrinGhost est un provider OIDC custom + intermédiaire de paiement. Crée un site dans le dashboard, liste tes actions, GrinGhost valide chaque débit et gère les crédits universels.

StarterRecommandé pour démarrer

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

Auth, catalogue d'actions, crédits universels et sandbox préconfigurés.

Parcours développeur#

De la création de compte au premier virement — voilà les 9 étapes pour être opérationnel sur GrinGhost.

1

Créer un compte GrinGhost

Connexion via Google OAuth sur gringhost.com. Ton compte est créé automatiquement.

2

Accepter les CGU/CGV

À la première ouverture du dashboard — acceptation obligatoire, horodatée et enregistrée.

3

Accepter les conditions développeur

À la première ouverture de l'espace développeur. Commission 10%, verrou 7j, préavis 30j, motifs de suspension.

4

Créer un site

Dashboard → Espace développeur → Nouveau site. Tu reçois clé API prod, clé API sandbox, OAuth client_id et client_secret.

5

Connecter ton compte Stripe

Dashboard dev → bannière « Connecter Stripe ». Stripe Express : Stripe gère le KYC et la vérification bancaire. Nécessaire pour recevoir tes payouts.

6

Enregistrer ton catalogue d'actions

Dashboard dev → ton site → + Action. Nom de l'action + coût en crédits. Le verrou tarifaire de 7j s'applique immédiatement.

7

Intégrer l'auth GrinGhost (OIDC)

Ajoute GrinGhost comme custom provider OIDC dans Supabase. Tes utilisateurs se connectent avec leur compte GrinGhost via un bouton « Continue with GrinGhost ».

8

Implémenter session-token + debit

Côté serveur : POST /api/site/session-token (route prepare). @gringhost/react vérifie le prix et affiche la confirmation. Côté serveur : POST /api/site/debit (route execute). Chaque crédit débité = consentement explicite prouvé.

9

Recevoir tes payouts

Dashboard dev → Retirer dès 50 000 crédits en attente (~$4.50). Virement direct sur ton compte Stripe sous 2–7 jours ouvrés.

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 site dans le dashboard GrinGhost

Dashboard → Espace développeur → Mes Sites → Nouveau site. Renseigne le nom et l'URL. Tu obtiens :

  • Clé API prod + Clé API sandbox — pour les appels backend
  • OAuth client_id + OAuth client_secret — pour le flux OIDC
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 du site — GrinGhost valide que le redirect_uri passé lors du flux OAuth a le même origin que l'URL enregistrée. En dev, enregistre http://localhost:3000.

2. Créer ton catalogue d'actions

Avant d'appeler l'API, enregistre tes actions dans le dashboard : Dashboard → Mes Sites → ton site → Catalogue d'actions.

Chaque action a un nom unique, une description optionnelle, et un prix en crédits. GrinGhost n'autorisera aucun débit sur une action non enregistrée.

text
Exemple de catalogue :
  analyze_image   → 500 crédits   (Analyser une image)
  generate_text   → 300 crédits   (Générer du texte)
  summarize       → 200 crédits   (Résumer un document)
Tu ne peux pas augmenter le prix d'une action dans les 7 jours suivant sa création ou sa dernière modification. Ce verrou protège tes utilisateurs.

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://www.gringhost.comhttps://www.gringhost.com
Client IDoauth_client_idoauth_client_id
Client Secretoauth_client_secretoauth_client_secret
Supabase auto-découvre les endpoints via https://www.gringhost.com/.well-known/openid-configuration. Le même oauth_client_id et oauth_client_secret s'utilisent en dev et en prod — la différence se fait sur la clé API (prod vs sandbox).

4. Ajouter le bouton de connexion

Avec le package @gringhost/react (recommandé) — place GrinGhostButton une fois dans ton header. Il gère login, menu, solde et confirmation automatiquement :

ts
npm install @gringhost/react
ts
// app/providers.tsx
'use client'
import { GrinGhostProvider } from '@gringhost/react'
import { createClient } from '@/lib/supabase/client'

const supabase = createClient()
export default function Providers({ children }: { children: React.ReactNode }) {
  return <GrinGhostProvider supabase={supabase}>{children}</GrinGhostProvider>
}

// app/layout.tsx — importe Providers
// Puis dans ton header :
import { GrinGhostButton } from '@gringhost/react'
<GrinGhostButton />  // pas de props — gère tout

Ou manuellement, sans le package :

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 + '/')
}

6. Récupérer le JWT GrinGhost côté serveur

Après le OAuth exchange, le JWT GrinGhost est stocké par Supabase comme provider_token. Tes routes serveur (prepare, execute) le lisent via le client Supabase server-side — il ne transite jamais côté client.

ts
// Dans toutes tes routes serveur (prepare, execute, credits…)
import { createClient } from '@/lib/supabase/server'

const supabase       = await createClient()
const { data: { user } }    = await supabase.auth.getUser()    // vérifie auth
const { data: { session } } = await supabase.auth.getSession()
const providerToken  = session?.provider_token  // JWT GrinGhost → body de /api/site/session-token
Ne jamais lire ni transmettre le provider_token côté client. Il est utilisé uniquement dans les routes serveur pour appeler /api/site/session-token.

7. Débiter des crédits — flux prepare/execute

Le débit suit un protocole en 4 temps : le serveur du dev génère un session token, @gringhost/react vérifie le prix directement depuis GrinGhost, l'utilisateur confirme dans GrinGhostButton, puis le serveur exécute le débit.

Route prepare — génère le session token côté serveur :

ts
// app/api/internal/mon-action/prepare/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function POST() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })

  const { data: { session } } = await supabase.auth.getSession()
  const providerToken = session?.provider_token
  if (!providerToken) return NextResponse.json({ error: 'no_gringhost_token' }, { status: 403 })

  const res = await fetch(`${process.env.GRINGHOST_BASE_URL}/api/site/session-token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.GRINGHOST_API_KEY! },
    body: JSON.stringify({ action_id: process.env.GRINGHOST_ACTION_ID, user_access_token: providerToken }),
  })
  const data = await res.json()
  if (res.status === 402) return NextResponse.json({ error: 'insufficient_credits' }, { status: 402 })
  if (res.status === 401) return NextResponse.json({ error: 'session_expired' }, { status: 401 })
  if (!res.ok)            return NextResponse.json({ error: 'session_token_failed' }, { status: 500 })

  return NextResponse.json({ token: data.token, credits_cost: data.credits_cost, action_name: data.action_name })
}
GRINGHOST_API_KEY reste sur le serveur — jamais exposée côté client. GRINGHOST_BASE_URL vaut https://www.gringhost.com.

Route execute — débite et appelle l'IA :

ts
// app/api/internal/mon-action/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })

  const { input, session_token } = await request.json()

  const debitRes = await fetch(`${process.env.GRINGHOST_BASE_URL}/api/site/debit`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.GRINGHOST_API_KEY! },
    body: JSON.stringify({ session_token, idempotency_key: crypto.randomUUID() }),
  })
  if (debitRes.status === 402) return NextResponse.json({ error: 'insufficient_credits' }, { status: 402 })
  if (!debitRes.ok)            return NextResponse.json({ error: 'debit_failed' }, { status: 500 })

  // Appel IA ici
  return NextResponse.json({ result: '...' })
}

Côté client — useGrinGhostAction déclenche la confirmation dans GrinGhostButton :

ts
'use client'
import { useGrinGhost, useGrinGhostAction } from '@gringhost/react'

const { loadCredits } = useGrinGhost()
const { prepare } = useGrinGhostAction('/api/internal/mon-action/prepare')

const result = await prepare()   // affiche la confirmation (prix vérifié, countdown 28s)
if (!result) return              // annulé ou timeout

const res = await fetch('/api/internal/mon-action', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ input, session_token: result.sessionToken }),
})

if (res.status === 402) { window.open('https://www.gringhost.com/buy', '_blank'); return }
loadCredits()  // rafraîchit le solde dans GrinGhostButton
Génère toujours un idempotency_key frais par tentative côté serveur (crypto.randomUUID()). En cas de retry réseau, GrinGhost retourne success: true sans débiter à nouveau.

8. Lire le solde de crédits

Avec @gringhost/react — le solde est chargé automatiquement à la connexion et rechargeable à la demande :

ts
'use client'
import { useGrinGhost } from '@gringhost/react'

const { credits, loadCredits } = useGrinGhost()
// credits est mis à jour automatiquement après chaque action
// loadCredits() force un rechargement depuis /api/internal/credits

Ou directement via les endpoints GrinGhost (authentifié par le JWT GrinGhost) :

ts
// Via l'endpoint wallet
const res = await fetch('https://www.gringhost.com/api/wallet', {
  headers: { 'Authorization': `Bearer ${gringhostAccessToken}` },
})
const { credits, sandbox_credits } = await res.json()

// Via userinfo
const res = await fetch('https://www.gringhost.com/api/oauth/userinfo', {
  headers: { 'Authorization': `Bearer ${gringhostAccessToken}` },
})
const { sub, email, credits, sandbox_credits } = await res.json()

9. Recharger des crédits

Quand un utilisateur manque de crédits (402), redirige-le vers la page d'achat GrinGhost — le wallet est universel, les crédits achetés sont utilisables sur tous les sites :

ts
if (res.status === 402) {
  window.open('https://www.gringhost.com/buy', '_blank')
}

Catalogue d'actions#

Le catalogue est la liste de tes actions IA avec leur prix en crédits. GrinGhost refuse tout débit sur une action non enregistrée ou inactive.

Règles du catalogue

  • Chaque action a un nom unique, une description optionnelle, et un prix en crédits (entier positif)
  • Une action peut être désactivée (is_active: false) sans être supprimée
  • Le prix est verrouillé 7 jours après création ou modification — impossible d'augmenter pendant ce délai
  • Tu peux ajouter de nouvelles actions à tout moment

Afficher le prix avant débit

@gringhost/react gère ça automatiquement. Quand useGrinGhostAction appelle ta route prepare, le package vérifie le prix directement depuis GrinGhost, puis affiche la confirmation dans GrinGhostButton (action_name, credits_cost, countdown 28s). Aucun code supplémentaire côté dev.

Le prix affiché à l'utilisateur vient de /api/public/session-token-info — appelé directement depuis le navigateur vers GrinGhost, sans passer par ton serveur. Impossible de falsifier le montant montré.

Session token#

Un session token est un jeton usage unique (TTL 30 secondes) créé depuis ton backend serveur. Il prouve que le débit correspond à une action enregistrée pour cet utilisateur — GrinGhost refuse tout appel à /api/site/debit sans ce token.

Pourquoi ce mécanisme ?

  • Empêche le dev de débiter en background sans action utilisateur
  • Le prix est fixé dans le token à la création — le dev ne peut pas le modifier entre session-token et debit
  • TTL 30 secondes — usage unique, pas de replay
  • Lié à un triplet (user_id, site_id, action_id) — non transférable

Mode service — widget B2B2C#

Le mode service permet à un utilisateur GrinGhost (ex : un restaurateur) de pré-autoriser un site à le débiter côté serveur, sans confirmation par action. Idéal pour les widgets chatbot, les SaaS à l'usage, ou tout flux où l'utilisateur final n'interagit pas directement avec GrinGhost.

Cas d'usage type

1

Le restaurateur crée un compte GrinGhost, achète des crédits.

2

Dans son dashboard GrinGhost → "Autorisations de service", il sélectionne ton site et fixe un budget total (ex. 500 000 crédits).

3

Il reçoit un service_token (ghsvc_xxx) — affiché une seule fois. Il le configure dans ton backend.

4

Ton backend débite ses crédits à chaque requête du chatbot, sans confirmation utilisateur.

5

Quand le budget est épuisé, le débit est refusé — le restaurateur recharge ou crée une nouvelle autorisation.

Côté backend — appel debit avec service_token

Remplace session_token par service_token + action_id. Pas de route prepare, pas de @gringhost/react — tout se passe côté serveur.

ts
// Exemple : une requête chatbot côté serveur
export async function POST(request: NextRequest) {
  const { message } = await request.json()

  const debitRes = await fetch(`${process.env.GRINGHOST_BASE_URL}/api/site/debit`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.GRINGHOST_API_KEY! },
    body: JSON.stringify({
      service_token:    process.env.GRINGHOST_SERVICE_TOKEN, // ghsvc_xxx
      action_id:        process.env.GRINGHOST_ACTION_ID,     // UUID de l'action "chatbot_query"
      idempotency_key:  crypto.randomUUID(),
    }),
  })

  if (debitRes.status === 402) {
    const { error } = await debitRes.json()
    if (error === 'budget_exceeded')     return NextResponse.json({ error: 'budget_épuisé' }, { status: 402 })
    if (error === 'insufficient_credits') return NextResponse.json({ error: 'crédits_insuffisants' }, { status: 402 })
  }
  if (!debitRes.ok) return NextResponse.json({ error: 'debit_failed' }, { status: 500 })

  // Appel IA
  return NextResponse.json({ result: '...' })
}
Le service_token n'est pas lié à un utilisateur final — c'est le restaurateur qui est débité, pas le visiteur du chatbot. Assure-toi que le restaurateur comprend qu'il paye pour chaque requête de ses clients.
Différence avec le mode session : le mode service est toujours live (pas de sandbox), le coût vient du catalogue (action_id), et il n'y a pas de GrinGhostButton ni de confirmation utilisateur. Le budget est contrôlé par le restaurateur via son dashboard.

Réponses /api/site/debit en mode service

json
// Succès (200)
{ "success": true, "remaining_budget": 485000 }

// Budget épuisé (402)
{ "error": "budget_exceeded", "remaining": 0 }

// Crédits insuffisants dans le wallet (402)
{ "error": "insufficient_credits", "remaining": 1200 }

// Token révoqué (403)
{ "error": "service_token_revoked" }

// Action introuvable ou inactive (404 / 403)
{ "error": "action_not_found" }
{ "error": "action_inactive" }

Sandbox — tester sans crédits réels#

En sandbox, les crédits ne sont pas réels — chaque utilisateur a 1 000 000 de crédits fictifs. La validation du catalogue reste active.

SandboxLive
Crédits débitésNon — fictifs (wallet sandbox)Oui — wallet universel
Validation de l'actionOuiOui
session_token requisOuiOui
Flux OAuthIdentiqueIdentique
Badge page auth"MODE SANDBOX"Normal
x-api-key à utilisersandbox_api_keyapi_key

Variables d'environnement

bash
# .env.local
GRINGHOST_BASE_URL=https://www.gringhost.com
GRINGHOST_API_KEY=<sandbox_api_key>    # sandbox_api_key en dev, api_key en prod
GRINGHOST_IS_SANDBOX=true              # false en prod

Réinitialiser les crédits sandbox

ts
// Remet le sandbox_credits à 1 000 000
const res = await fetch('https://www.gringhost.com/api/site/sandbox/reset', {
  method: 'POST',
  headers: { 'x-api-key': process.env.GRINGHOST_API_KEY! },
  body: JSON.stringify({ user_access_token: gringhostAccessToken }),
})

Passer en production

  1. Remplacer GRINGHOST_API_KEY par la clé live (api_key) dans Vercel env
  2. Passer GRINGHOST_IS_SANDBOX à false

Cas 2 — Autre framework#

À venir

Pour les stacks sans Supabase (Laravel, Express, Django…). Même flux OAuth standard. En attendant, utilise le Cas 1 avec Supabase.

Référence API#

Endpoints OIDC

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, credits, sandbox_credits

POST /api/site/session-token

Appelé depuis ton backend serveur (route prepare). Auth par x-api-key + JWT utilisateur (provider_token) dans le body. Ne jamais appeler depuis le navigateur — ça exposerait ta clé API.

Paramètre bodyTypeDescription
action_idstring (UUID)ID de l'action dans le catalogue
user_access_tokenstringGrinGhost JWT (provider_token Supabase)
json
// Succès (200)
{
  "token":        "uuid-du-session-token",  // TTL 30 secondes
  "credits_cost": 500,
  "action_name":  "Analyser une image",
  "expires_at":   "2026-06-14T10:32:30.000Z",
  "user_balance": 4500
}

// Crédits insuffisants (402)
{ "error": "insufficient_credits", "user_balance": 200 }

// Token utilisateur invalide (401)
{ "error": "invalid_token" }

// Action inactive ou introuvable (404 / 400)
{ "error": "action_not_found" }
{ "error": "action_inactive" }

GET /api/public/session-token-info

Endpoint public — pas d'auth. Appelé directement depuis le navigateur par @gringhost/react pour vérifier le prix d'un token avant d'afficher la confirmation.

text
GET /api/public/session-token-info?token=<session_token>
json
// Succès (200)
{
  "credits_cost": 1,
  "action_name":  "Chat IA",
  "expires_at":   "2026-06-15T10:32:30.000Z"
}

// Token introuvable ou expiré (404)
{ "error": "not_found" }
Utilisé en interne par useGrinGhostAction — tu n'as pas à l'appeler manuellement. Le prix vient directement de la DB GrinGhost, pas de ton serveur.

POST /api/site/debit

Appelé depuis ton backend serveur. Auth par x-api-key. Deux modes : session (B2C) ou service (widget B2B2C).

Mode session (B2C)

Paramètre bodyTypeDescription
session_tokenstring (UUID)Token généré par /api/site/session-token
idempotency_keystringClé unique pour éviter les doublons

Mode service (widget B2B2C)

Paramètre bodyTypeDescription
service_tokenstringToken ghsvc_xxx créé dans le dashboard
action_idstring (UUID)ID de l'action dans le catalogue
idempotency_keystringClé unique pour éviter les doublons
json
// Succès mode session (200)
{ "success": true }

// Succès mode service (200)
{ "success": true, "remaining_budget": 485000 }

// Token expiré — mode session (400)
{ "error": "token_expired" }

// Token déjà utilisé — mode session (409)
{ "error": "token_already_used" }

// Crédits insuffisants (402)
{ "error": "insufficient_credits", "remaining": 1200 }

// Budget service épuisé (402)
{ "error": "budget_exceeded", "remaining": 0 }

// Clé API invalide (401)
{ "error": "invalid_api_key" }

GET /api/wallet

Authentifié par le JWT GrinGhost (Authorization: Bearer).

json
{
  "credits":         4500,   // wallet prod (universel)
  "sandbox_credits": 985000, // wallet sandbox
  "updated_at":      "2026-06-14T09:15:00.000Z"
}

GET /api/site/ledger

Authentifié par x-api-key. Retourne les 100 dernières transactions de ton site.

json
{
  "ledger": [
    { "id": "...", "user_id": "...", "action_id": "...", "credits": 500,
      "dev_share": 450, "platform_share": 50, "is_sandbox": false,
      "idempotency_key": "...", "created_at": "..." }
  ],
  "is_sandbox": false
}

GET /api/site/balance

json
{
  "site_id": "...", "site_name": "Mon Site",
  "pending_credits":      12500,  // crédits accumulés depuis le dernier payout
  "total_earned_credits": 85000,  // total historique
  "last_payout_at":       "2026-05-01T..."
}

Tarifs & verrou tarifaire#

Tu fixes le prix de chaque action dans le catalogue. Les crédits sont universels — l'utilisateur achète sur gringhost.com, pas sur ton site.

Tes gains : 90% des crédits consommés × $0,0001 — soit $0.000090 par crédit.
ts
// Calculer le coût en crédits d'une action
// 1 crédit = 0.0001 $ payé par l'utilisateur → tu reçois 0.000090 $
const coutReel = 0.00005   // $ — ton coût API (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)
ActionCoût API×2CréditsTu reçois
GPT-4o-mini court (~300 tok)~$0.00005$0.00011 cr~$0.000090
GPT-4o-mini moyen (~1k tok)~$0.0002$0.00044 cr~$0.00036
GPT-4o moyen (~2k tok)~$0.005$0.01100 cr~$0.009
DALL-E 3~$0.04$0.08800 cr~$0.072
Verrou tarifaire — Tu ne peux pas augmenter le prix d'une action dans les 7 jours suivant sa création ou sa dernière modification. Ce délai protège les utilisateurs contre toute dévaluation.

Gains & virements#

Tu reçois 90% des crédits consommés sur ton site. Les gains s'accumulent automatiquement — déclenche un payout depuis le dashboard quand tu atteins 50 000 crédits (~$4.50 après commission).

Pack achetéPrixGrinGhost (10%)Tu reçois (90%)
Starter$5.00$0.50$4.50
Standard$20.00$2.00$18.00
Pro$50.00$5.00$45.00
La commission est calculée sur les crédits consommés, pas sur les crédits achetés. Les crédits non dépensés ne génèrent pas de revenus.

Connecter ton compte Stripe

Avant de pouvoir retirer tes gains, tu dois connecter un compte Stripe via Stripe Connect Express. GrinGhost s'occupe de l'intégration — tu n'as pas à créer un compte marchand Stripe indépendant.

1Dashboard dev → bannière violette « Connecte ton compte Stripe »
2Redirigé vers Stripe Express Onboarding — Stripe vérifie ton identité et ton RIB/IBAN
3Retour sur le dashboard → bouton « Retirer » activé sur chaque site
4Clique « Retirer » dès 50 000 crédits en attente → virement réel sous 2–7 jours ouvrés
GrinGhost utilise Stripe Connect Express — Stripe gère le KYC, la vérification bancaire et la conformité. Tu n'as pas à gérer la conformité PCI. Minimum de retrait : 50 000 crédits.

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 (session-token, debit, 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 : 300 crédits (action: generate_titles)."

"Crée une page /summarize. L'utilisateur colle un texte, clique Résumer.
Coût : 200 crédits (action: summarize)."
Claude Code ne modifiera pas l'auth ni la logique GrinGhost existante. Voir le starter →