Ürün

AutoAds

Ne sattığınızın tarifinden bitmiş reklam metni ve kurgulanmış görsel üreten bir platform. Her üretimin fiyatını çalışmadan önce gösteriyor.

Dört hamle, ve üstlerinde bir konsol

Neyin reklamının yapıldığını, kime gideceğini ve hangi markaya benzemesi gerektiğini tanımlıyorsunuz. Platform bu tanımlardan yola çıkıp önce metinle kuruyor: Claude brief’i doğrudan okuyup yazıyor, araya elle kopyalanan bir prompt girmiyor. Sonra metni artboard üzerine katman katman oturtan bir stüdyoda üretiyorsunuz; pan, zoom ve şerit başına yeniden deneme burada duruyor. Son olarak hiçbir şeye sahip olmayan, yalnızca bir görünüm olan galeride topluyorsunuz.

Dördünün üstünde Runs duruyor. Bu konsol iki soruyu cevaplıyor ve başkasını cevaplamıyor: ne oluyor, ve ne kadara mal oldu.

Bu ayrım ürünün kendisi oluyor. Sonradan eklenen her yetenek, “çıktısı nerede yaşayacak” sorusunu yeniden tartışmak yerine hazır cevabı devralıyor.

Kaydet düğmesi yok

Ürünün tamamında tek bir düzenleme hareketi var: promote. Tut yok, favori yok, yıldız yok. Üretilen her şey otomatik kaydediliyor, kişinin yaptığı tek iş de bir kompozisyonu concept’in ana görseli mertebesine çıkarmak oluyor.

Bunun dayattığı kural aslında bir tasarım testi: bir yüzeyin “bunu kaydet” düğmesine ihtiyacı varsa, o yüzeyin sorumluluğu yanlış kurulmuş demek. İçeriğine sahip olan bir galeri sessizce ikinci bir doğruluk kaynağına dönüşüyor ve aynı görsel için iki kaynak, altı ay sonra bulacağınız hata oluyor.

Kiracı ayrımı şemada duruyor

Her domain tablosu boş geçilemeyen, indeksli bir tenant id taşıyor ve her repository sorgusu veri katmanında kapsanıyor. Kapsama isteğe, token’a ya da birinin uygulamayı unutabileceği bir middleware’e bağlı kalmıyor. Kiracılar arası okuma yalnızca denetlenen bir withMasterScope(reason) sarmalayıcısının arkasında yaşıyor. Kural, bu ürünün fork edildiği altyapıdan geliyor; fork onu genişletiyor, hiç düzenlemiyor.

Alternatif, girişte token’ı okuyan bir guard’dı. Daha az kod tutuyor ve açığa düşüyor: repository’yi doğrudan sorgulayan tek bir controller başka takımın satırlarını sızdırır, tip sistemi de buna itiraz etmez.

GöstergeDoğrudan yol
  1. İstemciler

    • Stüdyo

      Next.js · React · TypeScript

  2. Sözleşme

    • Platform API'si

      NestJS · TypeScript · Bun

  3. Uygulama

    • Tanımlar

      TypeScript

      Neden

      Neyin reklamı yapılıyor, kime gidiyor ve hangi markaya benzemesi gerekiyor. Sonraki her aşama briefin kendi kopyasını taşımak yerine buradan okuyor.

    • Üretim hattı

      TypeScript

      Neden

      Tanımlar bir Concept'e, Concept kompozisyonlara dönüşüyor; kompozisyon da bir medya varlığının üzerindeki katmanlardan oluşuyor. Her aşamanın kendi adresi var, böylece bir koşu brief'e dönmek yerine ortasından yeniden girebiliyor.

  4. Ölçüm

    • Ölçümlü geçitler

      TypeScript

      Neden

      Para harcayan her çağrı buradan çözülüyor, harcama kaydı da geçerken isteği yapan takımın hanesine yazılıyor. Ölçüm çağrı döndükten sonra işliyor; başarısız bir üretim bu yüzden hiçbir ücret çıkarmıyor.

  5. Üretim

    • Metin üretimi

      Claude · TypeScript

    • Görsel üretimi

      TypeScript

      Neden

      Port, sağlayıcıyı ve modeli isteğin parçası olarak taşıyor; yani bir şeridi hangi sağlayıcının çizeceğine deployment başına değil çağrı başına karar veriliyor. Fiyatlama, kilitleme, kalıcılık ve silme bu seçimin üstünde duruyor ve onu hiç öğrenmiyor.

  6. Durum

    • İlişkisel veritabanı

      PostgreSQL · Drizzle

      Neden

      Her domain tablosu boş geçilemeyen, indeksli bir tenant id taşıyor ve her repository sorgusu istek katmanında değil veri katmanında kapsanıyor.

    • Varlık deposu

      TypeScript

Parayı harcayan her çağrı sayaçtan geçiyorTypeScriptÖlçümlü geçitler düğümünden
/** * The image sibling of `MeteredLlmGateway`. Every cost-bearing image call in * the product resolves `IMAGE_GENERATION` and therefore passes through here — * cost attribution is designed in from the first call, not retrofitted * (CLAUDE.md, Fork-0004 §5). */@Injectable()export class MeteredImageGateway implements ImageGenerationPort {  constructor(    @Inject(IMAGE_GENERATION_INNER) private readonly inner: ImageGenerationPort,    @Inject(USAGE_EVENTS) private readonly usage: UsageEventsPort,    @Inject(SPEND_METER) private readonly spend: SpendMeterPort,  ) {}  async generate(req: ImageGenerationRequest): Promise<ImageGenerationResult> {    // Metering after the await is deliberate: a failed generation charges    // nothing, matching MeteredLlmGateway.    const result = await this.inner.generate(req);    const { tenantId, userId, requestId, route } = req.attribution;    recordGenerationSpend(      { usage: this.usage, spend: this.spend },      {        tenantId,        userId,        // The port allows an unattributed call (a job has no HTTP request);        // `unknown` is the sentinel the rate-limit metering path already uses,        // and `request_id` is NOT NULL so something must stand in.        requestId: requestId ?? 'unknown',        route,        bucket: 'ai.image',        costKind: 'image_generation',        costUsd: result.costUsd,      },    );    return result;  }}
Ölçüm ikilisinin görsel tarafı. Üretim portu buradan geçmeden çözülmüyor, yani maliyet takibi her çağrı noktasının hatırlaması gereken bir iş olmaktan çıkıp wiring'in özelliği hâline geliyor. Kararı taşıyan iki ayrıntı var: ölçüm await'ten sonra işliyor, başarısız üretim bu yüzden hiçbir ücret çıkarmıyor; arka plandaki bir işten gelen ve sahibi belli olmayan çağrı ise adı konmuş bir sentinel'e düşüyor, çünkü kolon boş geçilemiyor ve yerine bir şey yazılması gerekiyor.

autoads4d9734cfapps/api/src/modules/media/adapters/out/metered-image-gateway.tsSatır 15 – 5137 satır

/**
 * The image sibling of `MeteredLlmGateway`. Every cost-bearing image call in
 * the product resolves `IMAGE_GENERATION` and therefore passes through here —
 * cost attribution is designed in from the first call, not retrofitted
 * (CLAUDE.md, Fork-0004 §5).
 */
@Injectable()
export class MeteredImageGateway implements ImageGenerationPort {
  constructor(
    @Inject(IMAGE_GENERATION_INNER) private readonly inner: ImageGenerationPort,
    @Inject(USAGE_EVENTS) private readonly usage: UsageEventsPort,
    @Inject(SPEND_METER) private readonly spend: SpendMeterPort,
  ) {}

  async generate(req: ImageGenerationRequest): Promise<ImageGenerationResult> {
    // Metering after the await is deliberate: a failed generation charges
    // nothing, matching MeteredLlmGateway.
    const result = await this.inner.generate(req);
    const { tenantId, userId, requestId, route } = req.attribution;
    recordGenerationSpend(
      { usage: this.usage, spend: this.spend },
      {
        tenantId,
        userId,
        // The port allows an unattributed call (a job has no HTTP request);
        // `unknown` is the sentinel the rate-limit metering path already uses,
        // and `request_id` is NOT NULL so something must stand in.
        requestId: requestId ?? 'unknown',
        route,
        bucket: 'ai.image',
        costKind: 'image_generation',
        costUsd: result.costUsd,
      },
    );
    return result;
  }
}
  • Ölçülecek bir şey yok. Ürünler, hedef kitleler, marka bileşenleri, hook'lar ve şablonlar sıradan kiracı kapsamlı okuma ve yazma işlemleri, o yüzden tek işi para harcayan çağrıyı fiyatlamak olan katmana uğramadan veritabanına gidiyorlar.

Üretimin parası

Üretim her çağrıda para harcıyor, o yüzden sayaç her çağrı noktasının hatırlaması gereken bir iş olmaktan çıkıp wiring’in parçası oluyor. Para harcayan her çağrı, sağlayıcıyı saran bir portu çözüyor ve harcama kaydı isteği yapan takımın hanesine düşüyor, çağrıyı bir insan da yapsa arka plandaki bir iş de yapsa.

Kullanıcının doğrudan hissettiği üç sonuç var:

  • Koşunun fiyatı çalışmadan önce görünüyor. Üretim çubuğu, henüz bir şeridi listeden çıkarmaya vakit varken seçili şeritlerin maliyetini gösteriyor.
  • Daha önce başarılı olmuş şerit, ücretsiz olarak bayt bayt yeniden kullanılıyor. Yeniden kullanım kilitleme üzerinden işliyor, yani yarısı düşmüş bir koşuyu tekrarlarken yalnızca düşen kısmın parası ödeniyor.
  • Ölçüm await’ten sonra işliyor. Başarısız bir üretim hiçbir ücret çıkarmıyor.

22

Bounded context

Her biri kendi tablolarının sahibi ve dışarıya satır değil port veriyor.

558

Test dosyası

Unit, slice, entegrasyon ve uçtan uca; dört ayrı proje halinde.

Katkı

Yazılan commit
1.749
Kaydedilen karar
66

14 haftanın 14 tanesi aktif · 11 May 2026 – 16 Ağu 2026

En uzun seri · 14 hafta · May – Ağu

Tek repo. Yanı sıra altmış altı karar kaydı yazıldı.

Ölçüm 2026-09-08