Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3df57cc
chore: remove deprecated Solana/wallet-based provider payouts
hankbobtheresearchoor May 12, 2026
c1c8bbe
remove profile.sh (unintended)
hankbobtheresearchoor May 12, 2026
285e3ab
fix: soft-fail Swift tests on dev + download full model for CI
ethenotethan May 13, 2026
3ccb06d
feat: environment-scoped R2 + coordinator secrets for dev/prod releas…
ethenotethan May 13, 2026
125e6fd
feat: DEV_/PROD_ prefixed repo secrets for R2 + coordinator env isola…
ethenotethan May 13, 2026
9185aca
fix: RELEASE_KEY is shared, not env-prefixed
ethenotethan May 13, 2026
a59ea9c
fix: resolve env secrets inline to avoid GitHub cross-job output masking
ethenotethan May 13, 2026
23313e4
fix: add DEV_RELEASE_KEY/PROD_RELEASE_KEY to env-prefixed secrets
ethenotethan May 13, 2026
d07827e
Add STRIDE threat model for runtime security review
anupsv May 14, 2026
01330bd
Expand threat model trust boundaries with implementation detail
anupsv May 14, 2026
f6fb8a2
Add threat model PR review workflow
anupsv May 14, 2026
4a0dae5
Persistent Secure Enclave key with keychain access group enforcement …
Gajesh2007 May 14, 2026
12ac05b
fix(api): add code and param fields to OpenAI error responses (#144)
hankbobtheresearchoor May 14, 2026
2e8f3ab
feat: add Datadog observability stack for dev coordinator (#143)
ethenotethan May 14, 2026
46bf2dd
Update billing_integration_test.go
Gajesh2007 May 14, 2026
a634508
fix: address review — CI failures, stale comments, logout message
hankbobtheresearchoor May 15, 2026
6e2a4b9
chore: resolve merge conflict in provider.go — keep DD metrics, remov…
hankbobtheresearchoor May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions coordinator/api/billing_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"io"
"log/slog"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -431,11 +431,11 @@ func TestIntegration_ReferralRewardDistribution(t *testing.T) {

// Verify provider got 95% of the charge.
// The provider in this test has no account linkage (connected via connectProvider),
// so payout goes to the wallet address. Since connectProvider does not set a
// wallet address, let's check the ledger's pending payouts instead.
// Provider without AccountID and without WalletAddress won't receive payout
// via CreditProvider (the code checks p.WalletAddress). But the cost flow is:
// handleComplete checks p.AccountID first, then p.WalletAddress.
// so payout goes to the provider address. Since connectProvider does not set a
// provider address, let's check the ledger's pending payouts instead.
// Provider without AccountID and without provider address won't receive payout
// via CreditProviderAccount. But the cost flow is:
// handleComplete checks p.AccountID first, then provider address.
// connectProvider doesn't set either, so no provider credit occurs.
// Verify the math is correct by checking the provider payout calculation.
if expectedProviderPayout != payments.ProviderPayout(totalCost) {
Expand Down Expand Up @@ -560,7 +560,7 @@ func TestIntegration_DeviceAuthFullFlow(t *testing.T) {
pubKey := testPublicKeyB64()
model := "device-auth-model"
models := []protocol.ModelInfo{{ID: model, ModelType: "chat", Quantization: "4bit"}}
conn := connectProviderWithToken(t, ctx, ts.URL, models, pubKey, tokenResult.Token, "0xDeviceTestWallet")
conn := connectProviderWithToken(t, ctx, ts.URL, models, pubKey, tokenResult.Token)
defer conn.Close(websocket.StatusNormalClosure, "")

// Wait for registration to complete.
Expand Down Expand Up @@ -664,14 +664,14 @@ func TestIntegration_MultiNodeSameAccount(t *testing.T) {

conn1 := connectProviderWithToken(t, ctx, ts.URL,
[]protocol.ModelInfo{{ID: model1, ModelType: "chat", Quantization: "4bit"}},
pubKey1, rawToken, "0xMultiNode1")
pubKey1, rawToken)
defer conn1.Close(websocket.StatusNormalClosure, "")

time.Sleep(200 * time.Millisecond)

conn2 := connectProviderWithToken(t, ctx, ts.URL,
[]protocol.ModelInfo{{ID: model2, ModelType: "chat", Quantization: "4bit"}},
pubKey2, rawToken, "0xMultiNode2")
pubKey2, rawToken)
defer conn2.Close(websocket.StatusNormalClosure, "")

time.Sleep(200 * time.Millisecond)
Expand Down
8 changes: 3 additions & 5 deletions coordinator/api/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2313,9 +2313,8 @@ func (s *Server) handleUsage(w http.ResponseWriter, r *http.Request) {

// handleProviderEarnings handles GET /v1/provider/earnings?wallet=0x...
//
// Returns the provider's balance and payout history by wallet address.
// No API key auth required — providers identify by wallet address.
// The wallet address is the same one sent during WebSocket registration.
// Returns the provider's balance and payout history.
// No API key auth required — providers identify by provider address.
func (s *Server) handleProviderEarnings(w http.ResponseWriter, r *http.Request) {
wallet := r.URL.Query().Get("wallet")
if wallet == "" {
Expand All @@ -2326,7 +2325,7 @@ func (s *Server) handleProviderEarnings(w http.ResponseWriter, r *http.Request)
return
}

// Look up balance by wallet address (same account ID used in CreditProvider)
// Look up balance by provider address
balance := s.ledger.Balance(wallet)
history := s.ledger.LedgerHistory(wallet)
payouts := s.ledger.AllPayouts()
Expand Down Expand Up @@ -2368,7 +2367,6 @@ func (s *Server) handleProviderEarnings(w http.ResponseWriter, r *http.Request)
}

writeJSON(w, http.StatusOK, map[string]any{
"wallet_address": wallet,
"balance_micro_usd": balance,
"balance_usd": fmt.Sprintf("%.6f", float64(balance)/1_000_000),
"total_earned_micro_usd": totalEarned,
Expand Down
13 changes: 7 additions & 6 deletions coordinator/api/consumer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1124,10 +1124,6 @@ func TestProviderEarningsEndpoint(t *testing.T) {
var resp map[string]any
json.Unmarshal(w.Body.Bytes(), &resp)

if resp["wallet_address"] != providerWallet {
t.Errorf("wallet_address = %v, want %v", resp["wallet_address"], providerWallet)
}

// Balance should be 450,000 + 900,000 = 1,350,000 micro-USD
balance := resp["balance_micro_usd"].(float64)
if balance != 1_350_000 {
Expand All @@ -1150,8 +1146,13 @@ func TestProviderEarningsUsesStoredPayoutRecords(t *testing.T) {
srv, _ := testServer(t)

wallet := "0xStoredPayoutWallet1234567890abcdef1234"
if err := srv.ledger.CreditProvider(wallet, 250_000, "qwen3.5-9b", "job-stored"); err != nil {
t.Fatalf("CreditProvider: %v", err)
if err := srv.store.CreditProviderWallet(&store.ProviderPayout{
ProviderAddress: wallet,
AmountMicroUSD: 250_000,
Model: "qwen3.5-9b",
JobID: "job-stored",
}); err != nil {
t.Fatalf("CreditProviderWallet: %v", err)
}

req := httptest.NewRequest(http.MethodGet, "/v1/provider/earnings?wallet="+wallet, nil)
Expand Down
13 changes: 3 additions & 10 deletions coordinator/api/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func connectProvider(t *testing.T, ctx context.Context, tsURL string, models []p
}

// connectProviderWithToken dials the WebSocket with an auth token.
func connectProviderWithToken(t *testing.T, ctx context.Context, tsURL string, models []protocol.ModelInfo, publicKey, authToken, walletAddress string) *websocket.Conn {
func connectProviderWithToken(t *testing.T, ctx context.Context, tsURL string, models []protocol.ModelInfo, publicKey, authToken string) *websocket.Conn {
t.Helper()
wsURL := "ws" + strings.TrimPrefix(tsURL, "http") + "/ws/provider"
conn, _, err := websocket.Dial(ctx, wsURL, nil)
Expand All @@ -141,7 +141,6 @@ func connectProviderWithToken(t *testing.T, ctx context.Context, tsURL string, m
EncryptedResponseChunks: true,
PrivacyCapabilities: testPrivacyCaps(),
AuthToken: authToken,
WalletAddress: walletAddress,
}
regData, _ := json.Marshal(regMsg)
if err := conn.Write(ctx, websocket.MessageText, regData); err != nil {
Expand Down Expand Up @@ -577,9 +576,8 @@ func TestIntegration_AccountLinkedEarnings(t *testing.T) {
pubKey := testPublicKeyB64()
model := "earnings-model"
models := []protocol.ModelInfo{{ID: model, ModelType: "test", Quantization: "4bit"}}
walletAddr := "0xProviderWalletShouldNotBeUsed"

conn := connectProviderWithToken(t, ctx, ts.URL, models, pubKey, rawToken, walletAddr)
conn := connectProviderWithToken(t, ctx, ts.URL, models, pubKey, rawToken)
defer conn.Close(websocket.StatusNormalClosure, "")

// Wait for registration + attestation to fully complete before
Expand Down Expand Up @@ -658,17 +656,12 @@ func TestIntegration_AccountLinkedEarnings(t *testing.T) {
// Give handleComplete a moment to process credits.
time.Sleep(300 * time.Millisecond)

// Verify the account received credits, not the wallet address.
// Verify the account received credits.
accountBalance := st.GetBalance(accountID)
if accountBalance <= 0 {
t.Errorf("account balance = %d, want > 0 (provider payout should be credited)", accountBalance)
}

walletBalance := st.GetBalance(walletAddr)
if walletBalance != 0 {
t.Errorf("wallet balance = %d, want 0 (account-linked provider should not credit wallet)", walletBalance)
}

// Verify provider earnings were recorded.
earnings, err := st.GetProviderEarnings(pubKey, 10)
if err != nil {
Expand Down
8 changes: 2 additions & 6 deletions coordinator/api/me_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ type myProvider struct {
EarningsTotalMicroUSD int64 `json:"earnings_total_micro_usd"`
EarningsCount int64 `json:"earnings_count"`

// Payout configuration
WalletAddress string `json:"wallet_address,omitempty"`
// Payout configuration (via Stripe Connect Express)

// Timestamps
RegisteredAt *time.Time `json:"registered_at,omitempty"`
Expand Down Expand Up @@ -134,7 +133,6 @@ type myFleetCounts struct {
// mySummaryResponse is the page-level dashboard header at /v1/me/summary.
type mySummaryResponse struct {
AccountID string `json:"account_id"`
WalletAddress string `json:"wallet_address,omitempty"`
AvailableBalanceMicroUSD int64 `json:"available_balance_micro_usd"`
WithdrawableBalanceMicroUSD int64 `json:"withdrawable_balance_micro_usd"`
PayoutReady bool `json:"payout_ready"`
Expand Down Expand Up @@ -199,10 +197,9 @@ func (s *Server) handleMySummary(w http.ResponseWriter, r *http.Request) {

resp := mySummaryResponse{
AccountID: accountID,
WalletAddress: user.SolanaWalletAddress,
AvailableBalanceMicroUSD: s.store.GetBalance(accountID),
WithdrawableBalanceMicroUSD: s.store.GetWithdrawableBalance(accountID),
PayoutReady: user.StripeAccountStatus == "ready" || user.SolanaWalletAddress != "",
PayoutReady: user.StripeAccountStatus == "ready",
LifetimeMicroUSD: summary.TotalMicroUSD,
LifetimeJobs: summary.Count,
Last24hMicroUSD: last24Money,
Expand Down Expand Up @@ -586,7 +583,6 @@ func buildMyProvider(rec *store.ProviderRecord, live *registry.Provider) myProvi
mp.LifetimeTokensGenerated = live.Stats.TokensGenerated
mp.PrefillTPS = live.PrefillTPS
mp.DecodeTPS = live.DecodeTPS
mp.WalletAddress = live.WalletAddress

if live.AttestationResult != nil {
ar := live.AttestationResult
Expand Down
16 changes: 3 additions & 13 deletions coordinator/api/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1003,11 +1003,11 @@ func (s *Server) handleComplete(providerID string, provider *registry.Provider,

// Calculate cost — check provider's custom price, then platform DB price,
// then hardcoded defaults.
providerWalletForPricing := ""
providerAccountForPricing := ""
if p := s.registry.GetProvider(providerID); p != nil {
providerWalletForPricing = p.WalletAddress
providerAccountForPricing = p.AccountID
}
customIn, customOut, hasCustom := s.store.GetModelPrice(providerWalletForPricing, pr.Model)
customIn, customOut, hasCustom := s.store.GetModelPrice(providerAccountForPricing, pr.Model)
if !hasCustom {
customIn, customOut, hasCustom = s.store.GetModelPrice("platform", pr.Model)
}
Expand Down Expand Up @@ -1093,16 +1093,6 @@ func (s *Server) handleComplete(providerID string, provider *registry.Provider,
"error", err,
)
}
} else if p.WalletAddress != "" {
// Unlinked provider — atomically credit the wallet and record payout history.
if err := s.ledger.CreditProvider(p.WalletAddress, providerPayout, pr.Model, msg.RequestID); err != nil {
s.logger.Error("failed to credit provider wallet payout",
"provider_id", providerID,
"wallet_address", p.WalletAddress,
"request_id", msg.RequestID,
"error", err,
)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion coordinator/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ func (s *Server) routes() {
s.mux.HandleFunc("GET /v1/payments/balance", s.requireAuth(s.handleBalance))
s.mux.HandleFunc("GET /v1/payments/usage", s.requireAuth(s.handleUsage))

// Provider earnings — no API key auth (providers identify by wallet address).
// Provider earnings — no API key auth (providers identify by provider address).
s.mux.HandleFunc("GET /v1/provider/earnings", s.handleProviderEarnings)

// Per-node provider earnings — public by provider_key, or auth'd by account.
Expand Down
29 changes: 7 additions & 22 deletions coordinator/auth/privy.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,9 @@ func (p *PrivyAuth) GetOrCreateUser(privyUserID string) (*store.User, error) {
}

user = &store.User{
AccountID: uuid.New().String(),
PrivyUserID: privyUserID,
Email: details.Email,
SolanaWalletAddress: details.WalletAddress,
SolanaWalletID: details.WalletID,
AccountID: uuid.New().String(),
PrivyUserID: privyUserID,
Email: details.Email,
}

if err := p.store.CreateUser(user); err != nil {
Expand All @@ -142,7 +140,6 @@ func (p *PrivyAuth) GetOrCreateUser(privyUserID string) (*store.User, error) {
"privy_user_id", privyUserID,
"account_id", user.AccountID,
"email", details.Email,
"has_wallet", details.WalletAddress != "",
)

return user, nil
Expand All @@ -155,19 +152,13 @@ type privyUserResponse struct {
}

type linkedAccount struct {
Type string `json:"type"`
Address string `json:"address,omitempty"`
ChainType string `json:"chain_type,omitempty"`
WalletID string `json:"wallet_client_type,omitempty"`
// For embedded wallets, the ID is in a nested field.
ID string `json:"id,omitempty"`
Type string `json:"type"`
Address string `json:"address,omitempty"`
}

// privyUserDetails holds extracted info from the Privy user API.
type privyUserDetails struct {
Email string
WalletAddress string
WalletID string
Email string
}

// fetchUserDetails calls Privy's REST API to get the user's email and wallet.
Expand Down Expand Up @@ -203,14 +194,8 @@ func (p *PrivyAuth) fetchUserDetails(privyUserID string) (*privyUserDetails, err
details := &privyUserDetails{}

for _, acct := range userResp.LinkedAccounts {
switch acct.Type {
case "email":
if acct.Type == "email" {
details.Email = acct.Address
case "wallet":
if acct.ChainType == "solana" {
details.WalletAddress = acct.Address
details.WalletID = acct.ID
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions coordinator/billing/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ type Config struct {
StripeConnectReturnURL string // where Stripe redirects after onboarding completes
StripeConnectRefreshURL string // where Stripe redirects if the link expires

// SolanaMnemonic is a BIP39 mnemonic phrase used to derive the coordinator's
// X25519 encryption key (via HKDF). Kept for backward compatibility with
// the e2e.DeriveCoordinatorKey() call path.
SolanaMnemonic string
// EncryptionMnemonic is a BIP39 mnemonic phrase used to derive the
// coordinator's X25519 encryption key (via HKDF) for sender→coordinator
// E2E request encryption (e2e.DeriveCoordinatorKey).
EncryptionMnemonic string

// Referral
ReferralSharePercent int64 // percentage of platform fee going to referrer (default 20)
Expand Down
8 changes: 3 additions & 5 deletions coordinator/billing/billing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,8 @@ func TestUserLifecycle(t *testing.T) {
st := store.NewMemory("")

user := &store.User{
AccountID: "acct-123",
PrivyUserID: "did:privy:abc",
SolanaWalletAddress: "SoLaNaAdDr123",
SolanaWalletID: "wallet-xyz",
AccountID: "acct-123",
PrivyUserID: "did:privy:abc",
}

if err := st.CreateUser(user); err != nil {
Expand All @@ -381,7 +379,7 @@ func TestUserLifecycle(t *testing.T) {
if err != nil {
t.Fatalf("get by privy: %v", err)
}
if got.AccountID != "acct-123" || got.SolanaWalletAddress != "SoLaNaAdDr123" {
if got.AccountID != "acct-123" {
t.Fatalf("unexpected: %+v", got)
}

Expand Down
2 changes: 1 addition & 1 deletion coordinator/billing/stripe_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const InstantFeeBps int64 = 150
const InstantFeeMinMicroUSD int64 = 500_000 // $0.50

// MinWithdrawMicroUSD is the smallest withdrawal accepted on the Stripe rail.
// $1 lines up with both Stripe's ACH minimum and the Solana withdraw minimum.
// $1 lines up with Stripe's ACH minimum.
const MinWithdrawMicroUSD int64 = 1_000_000

// FeeForInstantPayoutMicroUSD computes the platform fee for an instant payout
Expand Down
20 changes: 17 additions & 3 deletions coordinator/cmd/coordinator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,12 @@ func main() {

// Configure billing service (Stripe-only).
billingCfg := billing.Config{
// Mnemonic — used for coordinator encryption key derivation (e2e.DeriveCoordinatorKey).
SolanaMnemonic: envOr("MNEMONIC", os.Getenv("EIGENINFERENCE_SOLANA_MNEMONIC")),
// Mnemonic — used for coordinator encryption key derivation (e2e.DeriveCoordinatorKey).
EncryptionMnemonic: firstNonEmpty(
os.Getenv("MNEMONIC"),
os.Getenv("EIGENINFERENCE_MNEMONIC"),
os.Getenv("EIGENINFERENCE_SOLANA_MNEMONIC"), // legacy alias
),

// Stripe — primary payment rail for deposits.
StripeSecretKey: os.Getenv("EIGENINFERENCE_STRIPE_SECRET_KEY"),
Expand Down Expand Up @@ -354,7 +358,7 @@ func main() {
// with a coordinator-specific domain. Optional: dev environments without a
// mnemonic just get the /v1/encryption-key endpoint disabled (senders fall
// back to plaintext).
if coordKey, err := e2e.DeriveCoordinatorKey(billingCfg.SolanaMnemonic); err == nil {
if coordKey, err := e2e.DeriveCoordinatorKey(billingCfg.EncryptionMnemonic); err == nil {
srv.SetCoordinatorKey(coordKey)
logger.Info("sender→coordinator encryption enabled",
"kid", coordKey.KID,
Expand Down Expand Up @@ -536,6 +540,16 @@ func envOr(key, fallback string) string {
return fallback
}

// firstNonEmpty returns the first non-empty string from its arguments.
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}

func envFloat(key string, fallback float64) float64 {
if v := os.Getenv(key); v != "" {
if f, err := strconv.ParseFloat(v, 64); err == nil {
Expand Down
Loading