diff --git a/tariff/octopus.go b/tariff/octopus.go
index b79bf947d6..1a496573ad 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -3,21 +3,24 @@ package tariff
 import (
 	"errors"
 	"slices"
+	"strings"
 	"sync"
 	"time"
 
 	"github.com/cenkalti/backoff/v4"
 	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/tariff/octopus"
+	octoGql "github.com/evcc-io/evcc/tariff/octopus/graphql"
+	octoRest "github.com/evcc-io/evcc/tariff/octopus/rest"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 )
 
 type Octopus struct {
-	log    *util.Logger
-	uri    string
-	region string
-	data   *util.Monitor[api.Rates]
+	log         *util.Logger
+	region      string
+	productCode string
+	apikey      string
+	data        *util.Monitor[api.Rates]
 }
 
 var _ api.Tariff = (*Octopus)(nil)
@@ -28,26 +31,47 @@ func init() {
 
 func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) {
 	var cc struct {
-		Region string
-		Tariff string
+		Region      string
+		Tariff      string // DEPRECATED: use ProductCode
+		ProductCode string
+		ApiKey      string
 	}
 
+	logger := util.NewLogger("octopus")
+
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
-	if cc.Region == "" {
-		return nil, errors.New("missing region")
-	}
-	if cc.Tariff == "" {
-		return nil, errors.New("missing tariff code")
+	// Allow ApiKey to be missing only if Region and Tariff are not.
+	if cc.ApiKey == "" {
+		if cc.Region == "" {
+			return nil, errors.New("missing region")
+		}
+		if cc.Tariff == "" {
+			// deprecated - copy to correct slot and WARN
+			logger.WARN.Print("'tariff' is deprecated and will break in a future version - use 'productCode' instead")
+			cc.ProductCode = cc.Tariff
+		}
+		if cc.ProductCode == "" {
+			return nil, errors.New("missing product code")
+		}
+	} else {
+		// ApiKey validators
+		if cc.Region != "" || cc.Tariff != "" {
+			return nil, errors.New("cannot use apikey at same time as product code")
+		}
+		if len(cc.ApiKey) != 32 || !strings.HasPrefix(cc.ApiKey, "sk_live_") {
+			return nil, errors.New("invalid apikey format")
+		}
 	}
 
 	t := &Octopus{
-		log:    util.NewLogger("octopus"),
-		uri:    octopus.ConstructRatesAPI(cc.Tariff, cc.Region),
-		region: cc.Tariff,
-		data:   util.NewMonitor[api.Rates](2 * time.Hour),
+		log:         logger,
+		region:      cc.Region,
+		productCode: cc.ProductCode,
+		apikey:      cc.ApiKey,
+		data:        util.NewMonitor[api.Rates](2 * time.Hour),
 	}
 
 	done := make(chan error)
@@ -62,12 +86,35 @@ func (t *Octopus) run(done chan error) {
 	client := request.NewHelper(t.log)
 	bo := newBackoff()
 
+	var restQueryUri string
+
+	// If ApiKey is available, use GraphQL to get appropriate tariff code before entering execution loop.
+	if t.apikey != "" {
+		gqlCli, err := octoGql.NewClient(t.log, t.apikey)
+		if err != nil {
+			once.Do(func() { done <- err })
+			t.log.ERROR.Println(err)
+			return
+		}
+		tariffCode, err := gqlCli.TariffCode()
+		if err != nil {
+			once.Do(func() { done <- err })
+			t.log.ERROR.Println(err)
+			return
+		}
+		restQueryUri = octoRest.ConstructRatesAPIFromTariffCode(tariffCode)
+	} else {
+		// Construct Rest Query URI using tariff and region codes.
+		restQueryUri = octoRest.ConstructRatesAPIFromProductAndRegionCode(t.productCode, t.region)
+	}
+
+	// TODO tick every 15 minutes if GraphQL is available to poll for Intelligent slots.
 	tick := time.NewTicker(time.Hour)
 	for ; true; <-tick.C {
-		var res octopus.UnitRates
+		var res octoRest.UnitRates
 
 		if err := backoff.Retry(func() error {
-			return backoffPermanentError(client.GetJSON(t.uri, &res))
+			return backoffPermanentError(client.GetJSON(restQueryUri, &res))
 		}, bo); err != nil {
 			once.Do(func() { done <- err })
 
diff --git a/tariff/octopus/api.go b/tariff/octopus/api.go
deleted file mode 100644
index 1f96453a95..0000000000
--- a/tariff/octopus/api.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package octopus
-
-import (
-	"fmt"
-	"strings"
-	"time"
-)
-
-// ProductURI defines the location of the tariff information page. Substitute %s with tariff name.
-const ProductURI = "https://api.octopus.energy/v1/products/%s/"
-
-// RatesURI defines the location of the full tariff rates page, including speculation.
-// Substitute first %s with tariff name, second with region code.
-const RatesURI = ProductURI + "electricity-tariffs/E-1R-%s-%s/standard-unit-rates/"
-
-// ConstructRatesAPI returns a validly formatted, fully qualified URI to the unit rate information.
-func ConstructRatesAPI(tariff string, region string) string {
-	t := strings.ToUpper(tariff)
-	r := strings.ToUpper(region)
-	return fmt.Sprintf(RatesURI, t, t, r)
-}
-
-type UnitRates struct {
-	Count    uint64 `json:"count"`
-	Next     string `json:"next"`
-	Previous string `json:"previous"`
-	Results  []Rate `json:"results"`
-}
-
-type Rate struct {
-	ValidityStart     time.Time `json:"valid_from"`
-	ValidityEnd       time.Time `json:"valid_to"`
-	PriceInclusiveTax float64   `json:"value_inc_vat"`
-	PriceExclusiveTax float64   `json:"value_exc_vat"`
-}
diff --git a/tariff/octopus/graphql/api.go b/tariff/octopus/graphql/api.go
new file mode 100644
index 0000000000..0bb24e8725
--- /dev/null
+++ b/tariff/octopus/graphql/api.go
@@ -0,0 +1,147 @@
+package graphql
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/hasura/go-graphql-client"
+)
+
+// BaseURI is Octopus Energy's core API root.
+const BaseURI = "https://api.octopus.energy"
+
+// URI is the GraphQL query endpoint for Octopus Energy.
+const URI = BaseURI + "/v1/graphql/"
+
+// OctopusGraphQLClient provides an interface for communicating with Octopus Energy's Kraken platform.
+type OctopusGraphQLClient struct {
+	*graphql.Client
+
+	// apikey is the Octopus Energy API key (provided by user)
+	apikey string
+
+	// token is the GraphQL token used for communication with kraken (we get this ourselves with the apikey)
+	token *string
+	// tokenExpiration tracks the expiry of the acquired token. A new Token should be obtained if this time is passed.
+	tokenExpiration time.Time
+	// tokenMtx should be held when requesting a new token.
+	tokenMtx sync.Mutex
+
+	// accountNumber is the Octopus Energy account number associated with the given API key (queried ourselves via GraphQL)
+	accountNumber string
+}
+
+// NewClient returns a new, unauthenticated instance of OctopusGraphQLClient.
+func NewClient(log *util.Logger, apikey string) (*OctopusGraphQLClient, error) {
+	cli := request.NewClient(log)
+
+	gq := &OctopusGraphQLClient{
+		Client: graphql.NewClient(URI, cli),
+		apikey: apikey,
+	}
+
+	if err := gq.refreshToken(); err != nil {
+		return nil, err
+	}
+
+	// Future requests must have the appropriate Authorization header set.
+	gq.Client = gq.Client.WithRequestModifier(func(r *http.Request) {
+		gq.tokenMtx.Lock()
+		defer gq.tokenMtx.Unlock()
+		r.Header.Add("Authorization", *gq.token)
+	})
+
+	return gq, nil
+}
+
+// refreshToken updates the GraphQL token from the set apikey.
+// Basic caching is provided - it will not update the token if it hasn't expired yet.
+func (c *OctopusGraphQLClient) refreshToken() error {
+	// take a lock against the token mutex for the refresh
+	c.tokenMtx.Lock()
+	defer c.tokenMtx.Unlock()
+
+	if time.Until(c.tokenExpiration) > 5*time.Minute {
+		return nil
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	var q krakenTokenAuthentication
+	if err := c.Client.Mutate(ctx, &q, map[string]interface{}{"apiKey": c.apikey}); err != nil {
+		return err
+	}
+
+	c.token = &q.ObtainKrakenToken.Token
+	c.tokenExpiration = time.Now().Add(time.Hour)
+	return nil
+}
+
+// AccountNumber queries the Account Number assigned to the associated API key.
+// Caching is provided.
+func (c *OctopusGraphQLClient) AccountNumber() (string, error) {
+	// Check cache
+	if c.accountNumber != "" {
+		return c.accountNumber, nil
+	}
+
+	// Update refresh token (if necessary)
+	if err := c.refreshToken(); err != nil {
+		return "", err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	var q krakenAccountLookup
+	if err := c.Client.Query(ctx, &q, nil); err != nil {
+		return "", err
+	}
+
+	if len(q.Viewer.Accounts) == 0 {
+		return "", errors.New("no account associated with given octopus api key")
+	}
+	if len(q.Viewer.Accounts) > 1 {
+		return "", errors.New("more than one octopus account on this api key not supported")
+	}
+	c.accountNumber = q.Viewer.Accounts[0].Number
+	return c.accountNumber, nil
+}
+
+// TariffCode queries the Tariff Code of the first Electricity Agreement active on the account.
+func (c *OctopusGraphQLClient) TariffCode() (string, error) {
+	// Update refresh token (if necessary)
+	if err := c.refreshToken(); err != nil {
+		return "", err
+	}
+
+	// Get Account Number
+	acc, err := c.AccountNumber()
+	if err != nil {
+		return "", nil
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	var q krakenAccountElectricityAgreements
+	if err := c.Client.Query(ctx, &q, map[string]interface{}{"accountNumber": acc}); err != nil {
+		return "", err
+	}
+
+	if len(q.Account.ElectricityAgreements) == 0 {
+		return "", errors.New("no electricity agreements found")
+	}
+
+	// check type
+	//switch t := q.Account.ElectricityAgreements[0].Tariff.(type) {
+	//
+	//}
+	return q.Account.ElectricityAgreements[0].Tariff.TariffCode(), nil
+}
diff --git a/tariff/octopus/graphql/types.go b/tariff/octopus/graphql/types.go
new file mode 100644
index 0000000000..0a9b2f129c
--- /dev/null
+++ b/tariff/octopus/graphql/types.go
@@ -0,0 +1,79 @@
+package graphql
+
+// krakenTokenAuthentication is a representation of a GraphQL query for obtaining a Kraken API token.
+type krakenTokenAuthentication struct {
+	ObtainKrakenToken struct {
+		Token string
+	} `graphql:"obtainKrakenToken(input: {APIKey: $apiKey})"`
+}
+
+// krakenAccountLookup is a representation of a GraphQL query for obtaining the Account Number associated with the
+// credentials used to authorize the request.
+type krakenAccountLookup struct {
+	Viewer struct {
+		Accounts []struct {
+			Number string
+		}
+	}
+}
+
+type tariffData struct {
+	// yukky but the best way I can think of to handle this
+	// access via any relevant tariff data entry (i.e. standardTariff)
+	standardTariff   `graphql:"... on StandardTariff"`
+	dayNightTariff   `graphql:"... on DayNightTariff"`
+	threeRateTariff  `graphql:"... on ThreeRateTariff"`
+	halfHourlyTariff `graphql:"... on HalfHourlyTariff"`
+	prepayTariff     `graphql:"... on PrepayTariff"`
+}
+
+// TariffCode is a shortcut function to obtaining the Tariff Code of the given tariff, regardless of tariff type.
+// Developer Note: GraphQL query returns the same element keys regardless of type,
+// so it should always be decoded as standardTariff at least.
+// We are unlikely to use the other Tariff types for data access (?).
+func (d *tariffData) TariffCode() string {
+	return d.standardTariff.TariffCode
+}
+
+type tariffType struct {
+	Id                   string
+	DisplayName          string
+	FullName             string
+	ProductCode          string
+	StandingCharge       float32
+	PreVatStandingCharge float32
+}
+
+type tariffTypeWithTariffCode struct {
+	tariffType
+	TariffCode string
+}
+
+type standardTariff struct {
+	tariffTypeWithTariffCode
+}
+type dayNightTariff struct {
+	tariffTypeWithTariffCode
+}
+type threeRateTariff struct {
+	tariffTypeWithTariffCode
+}
+type halfHourlyTariff struct {
+	tariffTypeWithTariffCode
+}
+type prepayTariff struct {
+	tariffTypeWithTariffCode
+}
+
+type krakenAccountElectricityAgreements struct {
+	Account struct {
+		ElectricityAgreements []struct {
+			Id         int
+			Tariff     tariffData
+			MeterPoint struct {
+				// Mpan is the serial number of the meter that this ElectricityAgreement is bound to.
+				Mpan string
+			}
+		} `graphql:"electricityAgreements(active: true)"`
+	} `graphql:"account(accountNumber: $accountNumber)"`
+}
diff --git a/tariff/octopus/rest/api.go b/tariff/octopus/rest/api.go
new file mode 100644
index 0000000000..a1308edd3a
--- /dev/null
+++ b/tariff/octopus/rest/api.go
@@ -0,0 +1,48 @@
+package rest
+
+import (
+	"fmt"
+	"strings"
+	"time"
+)
+
+// ProductURI defines the location of the tariff information page. Substitute %s with tariff name.
+const ProductURI = "https://api.octopus.energy/v1/products/%s/"
+
+// RatesURI defines the location of the full tariff rates page, including speculation.
+// Substitute first %s with product code, second with tariff code.
+const RatesURI = ProductURI + "electricity-tariffs/%s/standard-unit-rates/"
+
+// ConstructRatesAPIFromProductAndRegionCode returns a validly formatted, fully qualified URI to the unit rate information
+// derived from the given product code and region.
+func ConstructRatesAPIFromProductAndRegionCode(product string, region string) string {
+	tCode := strings.ToUpper(fmt.Sprintf("E-1R-%s-%s", product, region))
+	return fmt.Sprintf(RatesURI, product, tCode)
+}
+
+// ConstructRatesAPIFromTariffCode returns a validly formatted, fully qualified URI to the unit rate information
+// derived from the given Tariff Code.
+func ConstructRatesAPIFromTariffCode(tariff string) string {
+	// Hacky bullshit, saves handling both the product and tariff codes in GQL mode.
+	// Hopefully Octopus don't change how this works otherwise we might have to do this properly :(
+	if len(tariff) < 7 {
+		// OOB check
+		return ""
+	}
+	pCode := tariff[5 : len(tariff)-2]
+	return fmt.Sprintf(RatesURI, pCode, tariff)
+}
+
+type UnitRates struct {
+	Count    uint64 `json:"count"`
+	Next     string `json:"next"`
+	Previous string `json:"previous"`
+	Results  []Rate `json:"results"`
+}
+
+type Rate struct {
+	ValidityStart     time.Time `json:"valid_from"`
+	ValidityEnd       time.Time `json:"valid_to"`
+	PriceInclusiveTax float64   `json:"value_inc_vat"`
+	PriceExclusiveTax float64   `json:"value_exc_vat"`
+}
diff --git a/templates/definition/tariff/octopus-api.yaml b/templates/definition/tariff/octopus-api.yaml
new file mode 100644
index 0000000000..6121f7b8af
--- /dev/null
+++ b/templates/definition/tariff/octopus-api.yaml
@@ -0,0 +1,14 @@
+template: octopus-api
+products:
+  - brand: Octopus Energy
+    description:
+      en: Octopus Energy - API
+params:
+  - name: apiKey
+    type: string
+    required: true
+    description:
+      en: "Your Octopus Energy API Key. You can find it here: https://octopus.energy/dashboard/new/accounts/personal-details/api-access"
+render: |
+  type: octopusenergy
+  apikey: {{ .apikey }}
diff --git a/templates/definition/tariff/octopus-productcode.yaml b/templates/definition/tariff/octopus-productcode.yaml
new file mode 100644
index 0000000000..0dfcd3edcd
--- /dev/null
+++ b/templates/definition/tariff/octopus-productcode.yaml
@@ -0,0 +1,20 @@
+template: octopus-productcode
+products:
+  - brand: Octopus Energy
+    description:
+      en: Octopus Energy - Product Code
+params:
+  - name: productCode
+    type: string
+    required: true
+    description:
+      en: "The tariff code for your energy contract. Make sure this is set to your import tariff code. It'll look something like this: AGILE-FLEX-22-11-25"
+  - name: region
+    type: string
+    required: true
+    description:
+      en: "The DNO region you are located in. More information: https://www.energy-stats.uk/dno-region-codes-explained/"
+render: |
+  type: octopusenergy
+  productCode: {{ .productCode }}
+  region: {{ .region }}