Files
zeekay b798da77d1 billing: take USDC top-ups through Circle Gateway
An agent or a person signs one EIP-3009 authorization against its Circle
Gateway balance, made out to the treasury, and is credited the moment
Gateway locks it. No human, no subscription, cards untouched.

  GET  /v1/billing/crypto/gateway      terms + a nonce issued to the caller
  POST /v1/billing/crypto/gateway      the signed payment
  GET  /v1/billing/crypto/gateway/:id  pending, accepted, settled, failed
  GET  /v1/commerce/treasury           superadmin: Gateway position

payment/providers/circle replaces the Payments API v1 provider with a
permissionless Gateway client and the GatewayWalletBatched EIP-712
encoding, proven against eth-account and Circle's testnet verifier.

billing/gateway checks a payment before Gateway sees it (treasury, live
network, whole cents in $1..$1,000, window, low-s signature from `from`,
nonce bound to the caller), settles it through /v1/x402/settle, credits
the host ledger keyed on the remittance, and reconciles by polling
/v1/x402/transfers: Gateway's webhooks report no batched payment.

Armed by CIRCLE_GATEWAY_TREASURY with CIRCLE_ENVIRONMENT; unset is inert.
2026-10-26 20:23:50 -07:00

68 lines
2.1 KiB
Go

// Copyright (c) 2014-present Hanzo AI, Inc.
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
package circle
import (
"fmt"
"strings"
)
// The deployment arms the rail with two plain values and no secret: Gateway
// takes no key for anything the rail calls.
//
// CIRCLE_GATEWAY_TREASURY the address payments are made TO — Hanzo's wallet.
// Unset: the rail is shut and nothing below is read.
// CIRCLE_ENVIRONMENT "production" (mainnet) or "sandbox" (testnet).
// Required once the treasury is set: which money is
// real is never a default.
const (
EnvTreasury = "CIRCLE_GATEWAY_TREASURY"
EnvNetwork = "CIRCLE_ENVIRONMENT"
)
// Config is the rail as a deployment arms it.
type Config struct {
Network Network
Treasury string // 0x + 40 lowercase hex; empty when the rail is not armed
}
// Armed reports whether a treasury is configured. An unarmed rail offers
// nothing and accepts nothing.
func (c Config) Armed() bool { return c.Treasury != "" }
// FromEnv reads the rail's configuration from environ (as os.Environ returns
// it). No treasury is the inert state, not an error. A treasury with an
// unreadable address or no stated environment IS an error: an operator who
// armed the rail must not get a process that takes payments into an address
// it misread, or on a network it guessed.
func FromEnv(environ []string) (Config, error) {
env := map[string]string{}
for _, kv := range environ {
if k, v, ok := strings.Cut(kv, "="); ok {
env[k] = strings.TrimSpace(v)
}
}
raw := env[EnvTreasury]
if raw == "" {
return Config{}, nil
}
addr, err := Address(raw)
if err != nil {
return Config{}, fmt.Errorf("%s: %w", EnvTreasury, err)
}
if addr == ([20]byte{}) {
return Config{}, fmt.Errorf("%s is the zero address", EnvTreasury)
}
var n Network
switch env[EnvNetwork] {
case "production":
n = Mainnet
case "sandbox":
n = Testnet
default:
return Config{}, fmt.Errorf("%s is set, so %s must be \"production\" or \"sandbox\", not %q", EnvTreasury, EnvNetwork, env[EnvNetwork])
}
return Config{Network: n, Treasury: Hex(addr[:])}, nil
}