@@ -50,6 +50,7 @@ func sellCommand(cfg *config.Config) *cli.Command {
5050 sellStatusCommand (cfg ),
5151 sellTestCommand (cfg ),
5252 sellStopCommand (cfg ),
53+ sellUpdateCommand (cfg ),
5354 sellDeleteCommand (cfg ),
5455 sellPricingCommand (cfg ),
5556 sellRegisterCommand (cfg ),
@@ -1334,6 +1335,137 @@ func sellStopCommand(cfg *config.Config) *cli.Command {
13341335 }
13351336}
13361337
1338+ // ---------------------------------------------------------------------------
1339+ // sell update
1340+ // ---------------------------------------------------------------------------
1341+
1342+ func sellUpdateCommand (cfg * config.Config ) * cli.Command {
1343+ return & cli.Command {
1344+ Name : "update" ,
1345+ Usage : "Update pricing or wallet on an existing ServiceOffer in place" ,
1346+ ArgsUsage : "<name>" ,
1347+ Description : `Patches a live ServiceOffer without deleting it. Only the fields you pass
1348+ are changed; everything else is preserved. The serviceoffer-controller will
1349+ reconcile the new payment config automatically.
1350+
1351+ Switching price models (e.g. per-request → per-mtok) nulls the previous keys
1352+ so the controller picks up the new model.
1353+
1354+ Examples:
1355+ obol sell update my-api -n llm --per-request 0.002
1356+ obol sell update my-api -n llm --per-mtok 5.0
1357+ obol sell update my-api -n llm --wallet 0xNew... --chain base` ,
1358+ Flags : []cli.Flag {
1359+ & cli.StringFlag {
1360+ Name : "namespace" ,
1361+ Aliases : []string {"n" },
1362+ Usage : "Namespace of the ServiceOffer" ,
1363+ Required : true ,
1364+ },
1365+ & cli.StringFlag {
1366+ Name : "wallet" ,
1367+ Aliases : []string {"w" },
1368+ Usage : "New USDC recipient wallet address" ,
1369+ },
1370+ & cli.StringFlag {
1371+ Name : "chain" ,
1372+ Usage : "New payment chain (base, base-sepolia, ethereum)" ,
1373+ },
1374+ & cli.StringFlag {
1375+ Name : "price" ,
1376+ Usage : "New per-request price in USDC (alias for --per-request)" ,
1377+ },
1378+ & cli.StringFlag {
1379+ Name : "per-request" ,
1380+ Usage : "New per-request price in USDC" ,
1381+ },
1382+ & cli.StringFlag {
1383+ Name : "per-mtok" ,
1384+ Usage : "New per-million-tokens price in USDC" ,
1385+ },
1386+ & cli.StringFlag {
1387+ Name : "per-hour" ,
1388+ Usage : "New per-compute-hour price in USDC" ,
1389+ },
1390+ },
1391+ Action : func (ctx context.Context , cmd * cli.Command ) error {
1392+ u := getUI (cmd )
1393+ if cmd .NArg () == 0 {
1394+ return errors .New ("name required: obol sell update <name> -n <ns> [--per-request N | --per-mtok N | --per-hour N] [--wallet 0x...] [--chain base]" )
1395+ }
1396+
1397+ name := cmd .Args ().First ()
1398+ if err := validate .Name (name ); err != nil {
1399+ return err
1400+ }
1401+ ns := cmd .String ("namespace" )
1402+
1403+ if _ , err := kubectlOutput (cfg , "get" , "serviceoffers.obol.org" , name , "-n" , ns , "-o" , "name" ); err != nil {
1404+ return fmt .Errorf ("ServiceOffer %s/%s not found: %w" , ns , name , err )
1405+ }
1406+
1407+ payment := map [string ]any {}
1408+
1409+ if wallet := strings .TrimSpace (cmd .String ("wallet" )); wallet != "" {
1410+ if err := x402verifier .ValidateWallet (wallet ); err != nil {
1411+ return err
1412+ }
1413+ payment ["payTo" ] = wallet
1414+ }
1415+
1416+ if chain := strings .TrimSpace (cmd .String ("chain" )); chain != "" {
1417+ payment ["network" ] = chain
1418+ }
1419+
1420+ priceSet := cmd .String ("price" ) != "" || cmd .String ("per-request" ) != "" || cmd .String ("per-mtok" ) != "" || cmd .String ("per-hour" ) != ""
1421+ if priceSet {
1422+ priceTable , err := resolvePriceTable (cmd , true )
1423+ if err != nil {
1424+ return err
1425+ }
1426+
1427+ price := map [string ]any {
1428+ "perRequest" : nil ,
1429+ "perMTok" : nil ,
1430+ "perHour" : nil ,
1431+ }
1432+ switch {
1433+ case priceTable .PerRequest != "" :
1434+ price ["perRequest" ] = priceTable .PerRequest
1435+ case priceTable .PerMTok != "" :
1436+ price ["perMTok" ] = priceTable .PerMTok
1437+ case priceTable .PerHour != "" :
1438+ price ["perHour" ] = priceTable .PerHour
1439+ }
1440+ payment ["price" ] = price
1441+ }
1442+
1443+ if len (payment ) == 0 {
1444+ return errors .New ("nothing to update: pass at least one of --per-request / --per-mtok / --per-hour / --wallet / --chain" )
1445+ }
1446+
1447+ patch := map [string ]any {
1448+ "spec" : map [string ]any {
1449+ "payment" : payment ,
1450+ },
1451+ }
1452+ patchBytes , err := json .Marshal (patch )
1453+ if err != nil {
1454+ return fmt .Errorf ("marshal patch: %w" , err )
1455+ }
1456+
1457+ if err := kubectlRun (cfg , "patch" , "serviceoffers.obol.org" , name , "-n" , ns , "--type=merge" , "-p" , string (patchBytes )); err != nil {
1458+ return fmt .Errorf ("failed to patch serviceoffer: %w" , err )
1459+ }
1460+
1461+ u .Successf ("ServiceOffer %s/%s updated" , ns , name )
1462+ u .Info ("The controller will reconcile the new payment config." )
1463+ u .Infof ("Check status: obol sell status %s -n %s" , name , ns )
1464+ return nil
1465+ },
1466+ }
1467+ }
1468+
13371469// ---------------------------------------------------------------------------
13381470// sell delete
13391471// ---------------------------------------------------------------------------
0 commit comments