@@ -293,10 +293,31 @@ enum Commands {
293293 #[ arg( long) ]
294294 show : bool ,
295295 } ,
296+ /// View credit balance, usage, and top up.
297+ Billing {
298+ #[ command( subcommand) ]
299+ command : Option < BillingCommands > ,
300+ } ,
296301 #[ command( external_subcommand) ]
297302 External ( Vec < String > ) ,
298303}
299304
305+ #[ derive( Debug , Subcommand ) ]
306+ enum BillingCommands {
307+ /// Show usage breakdown by service.
308+ Usage ,
309+ /// Show transaction history.
310+ History ,
311+ /// Show credit pricing table.
312+ Prices ,
313+ /// Buy credits (opens Stripe checkout in browser).
314+ Topup {
315+ /// Package slug: starter, standard, pro, enterprise.
316+ #[ arg( default_value = "starter" ) ]
317+ package : String ,
318+ } ,
319+ }
320+
300321#[ derive( Debug , Subcommand ) ]
301322enum WorkflowCommands {
302323 List ,
@@ -2140,6 +2161,152 @@ async fn main() -> Result<()> {
21402161 }
21412162 tui:: run_tui_app ( & project_root, & python) . await ?;
21422163 }
2164+ Commands :: Billing { command } => {
2165+ let ( api_base, auth) = resolve_agent_auth ( ) ?;
2166+ let client = reqwest:: Client :: builder ( )
2167+ . timeout ( Duration :: from_secs ( 15 ) )
2168+ . build ( ) ?;
2169+
2170+ match command {
2171+ None => {
2172+ // Default: show balance
2173+ let resp: serde_json:: Value = auth
2174+ . apply ( client. get ( format ! ( "{api_base}/billing/balance" ) ) )
2175+ . send ( )
2176+ . await ?
2177+ . error_for_status ( ) ?
2178+ . json ( )
2179+ . await ?;
2180+ println ! ( "\n MARC27 Credits" ) ;
2181+ println ! ( "\u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} " ) ;
2182+ println ! (
2183+ " Balance: {:.1} credits (${:.2})" ,
2184+ resp[ "credits" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2185+ resp[ "dollar_value" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2186+ ) ;
2187+ println ! (
2188+ " Org: {}" ,
2189+ resp[ "org_name" ] . as_str( ) . unwrap_or( "unknown" )
2190+ ) ;
2191+ println ! ( "\u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \u{2501} \n " ) ;
2192+ }
2193+ Some ( BillingCommands :: Usage ) => {
2194+ let resp: serde_json:: Value = auth
2195+ . apply ( client. get ( format ! ( "{api_base}/billing/usage?period=monthly" ) ) )
2196+ . send ( )
2197+ . await ?
2198+ . error_for_status ( ) ?
2199+ . json ( )
2200+ . await ?;
2201+ println ! ( "\n Usage (current period)\n " ) ;
2202+ if let Some ( services) = resp[ "by_service" ] . as_array ( ) {
2203+ for svc in services {
2204+ println ! (
2205+ " {:<30} {:.2} credits {} calls" ,
2206+ svc[ "metric" ] . as_str( ) . unwrap_or( "?" ) ,
2207+ svc[ "credits_spent" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2208+ svc[ "request_count" ] . as_u64( ) . unwrap_or( 0 ) ,
2209+ ) ;
2210+ }
2211+ }
2212+ println ! (
2213+ "\n Total: {:.2} credits\n " ,
2214+ resp[ "total" ] . as_f64( ) . unwrap_or( 0.0 )
2215+ ) ;
2216+ }
2217+ Some ( BillingCommands :: History ) => {
2218+ let resp: serde_json:: Value = auth
2219+ . apply ( client. get ( format ! ( "{api_base}/billing/history?page=1&per_page=20" ) ) )
2220+ . send ( )
2221+ . await ?
2222+ . error_for_status ( ) ?
2223+ . json ( )
2224+ . await ?;
2225+ println ! ( "\n Transaction History\n " ) ;
2226+ if let Some ( txns) = resp[ "transactions" ] . as_array ( ) {
2227+ for tx in txns {
2228+ println ! (
2229+ " {} {:+.2} credits {}" ,
2230+ tx[ "created_at" ] . as_str( ) . unwrap_or( "?" ) ,
2231+ tx[ "amount_credits" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2232+ tx[ "description" ] . as_str( ) . unwrap_or( "" ) ,
2233+ ) ;
2234+ }
2235+ if txns. is_empty ( ) {
2236+ println ! ( " No transactions yet." ) ;
2237+ }
2238+ }
2239+ println ! ( ) ;
2240+ }
2241+ Some ( BillingCommands :: Prices ) => {
2242+ let resp: serde_json:: Value = client
2243+ . get ( format ! ( "{api_base}/billing/prices" ) )
2244+ . send ( )
2245+ . await ?
2246+ . error_for_status ( ) ?
2247+ . json ( )
2248+ . await ?;
2249+ println ! ( "\n Credit Prices\n " ) ;
2250+ if let Some ( prices) = resp[ "prices" ] . as_array ( ) {
2251+ for p in prices {
2252+ println ! (
2253+ " {:<30} {:.4} credits/{} ({}% markup)" ,
2254+ p[ "metric" ] . as_str( ) . unwrap_or( "?" ) ,
2255+ p[ "credits_per_unit" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2256+ p[ "unit_label" ] . as_str( ) . unwrap_or( "unit" ) ,
2257+ p[ "markup_pct" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2258+ ) ;
2259+ }
2260+ }
2261+ println ! ( ) ;
2262+ }
2263+ Some ( BillingCommands :: Topup { package } ) => {
2264+ // First show packages
2265+ let pkgs: serde_json:: Value = client
2266+ . get ( format ! ( "{api_base}/billing/packages" ) )
2267+ . send ( )
2268+ . await ?
2269+ . error_for_status ( ) ?
2270+ . json ( )
2271+ . await ?;
2272+ println ! ( "\n Available packages:\n " ) ;
2273+ if let Some ( packages) = pkgs[ "packages" ] . as_array ( ) {
2274+ for ( i, p) in packages. iter ( ) . enumerate ( ) {
2275+ println ! (
2276+ " {}. {:<12} \u{2014} {} credits ${:.2}" ,
2277+ i + 1 ,
2278+ p[ "slug" ] . as_str( ) . unwrap_or( "?" ) ,
2279+ p[ "credits" ] . as_u64( ) . unwrap_or( 0 ) ,
2280+ p[ "price_usd" ] . as_f64( ) . unwrap_or( 0.0 ) ,
2281+ ) ;
2282+ }
2283+ }
2284+
2285+ println ! ( "\n Opening Stripe checkout for '{package}'..." ) ;
2286+ let resp: serde_json:: Value = auth
2287+ . apply ( client. post ( format ! ( "{api_base}/billing/topup" ) ) )
2288+ . json ( & serde_json:: json!( { "package" : package} ) )
2289+ . send ( )
2290+ . await ?
2291+ . error_for_status ( ) ?
2292+ . json ( )
2293+ . await ?;
2294+
2295+ if let Some ( url) = resp[ "checkout_url" ] . as_str ( ) {
2296+ println ! ( "Checkout: {url}\n " ) ;
2297+ if let Err ( e) = open_browser ( url) {
2298+ eprintln ! ( "Could not open browser: {e}" ) ;
2299+ println ! ( "Open the URL above manually." ) ;
2300+ }
2301+ } else {
2302+ eprintln ! (
2303+ "Error: {}" ,
2304+ resp[ "error" ] [ "message" ] . as_str( ) . unwrap_or( "unknown error" )
2305+ ) ;
2306+ }
2307+ }
2308+ }
2309+ }
21432310 Commands :: External ( args) => {
21442311 if try_run_workflow_alias ( & project_root, & args) . await ? {
21452312 return Ok ( ( ) ) ;
0 commit comments