diff --git a/models/forgefed/actor.go b/models/forgefed/actor.go
index 1f54566d41..b633b61514 100644
--- a/models/forgefed/actor.go
+++ b/models/forgefed/actor.go
@@ -36,7 +36,7 @@ type RepositoryID struct {
 }
 
 // newActorID receives already validated inputs
-func newActorID(validatedURI *url.URL, source string) (ActorID, error) {
+func NewActorID(validatedURI *url.URL) (ActorID, error) {
 	pathWithActorID := strings.Split(validatedURI.Path, "/")
 	if containsEmptyString(pathWithActorID) {
 		pathWithActorID = removeEmptyStrings(pathWithActorID)
@@ -47,20 +47,30 @@ func newActorID(validatedURI *url.URL, source string) (ActorID, error) {
 
 	result := ActorID{}
 	result.ID = id
-	result.Source = source
 	result.Schema = validatedURI.Scheme
 	result.Host = validatedURI.Hostname()
 	result.Path = pathWithoutActorID
 	result.Port = validatedURI.Port()
 	result.UnvalidatedInput = validatedURI.String()
 
-	if valid, err := IsValid(result); !valid {
-		return ActorId{}, err
+	if valid, outcome := validation.IsValid(result); !valid {
+		return ActorID{}, outcome
 	}
 
 	return result, nil
 }
 
+func newActorID(validatedURI *url.URL, source string) (ActorID, error) {
+	result, err := NewActorID(validatedURI)
+	if err != nil {
+		return ActorID{}, err
+	}
+
+	result.Source = source
+
+	return result, nil
+}
+
 func NewPersonID(uri, source string) (PersonID, error) {
 	// TODO: remove after test
 	//if !validation.IsValidExternalURL(uri) {
@@ -138,12 +148,10 @@ func (id PersonID) HostSuffix() string {
 func (id ActorID) Validate() []string {
 	var result []string
 	result = append(result, validation.ValidateNotEmpty(id.ID, "userId")...)
-	result = append(result, validation.ValidateNotEmpty(id.Source, "source")...)
 	result = append(result, validation.ValidateNotEmpty(id.Schema, "schema")...)
 	result = append(result, validation.ValidateNotEmpty(id.Path, "path")...)
 	result = append(result, validation.ValidateNotEmpty(id.Host, "host")...)
 	result = append(result, validation.ValidateNotEmpty(id.UnvalidatedInput, "unvalidatedInput")...)
-	result = append(result, validation.ValidateOneOf(id.Source, []string{"forgejo", "gitea"})...)
 
 	if id.UnvalidatedInput != id.AsURI() {
 		result = append(result, fmt.Sprintf("not all input: %q was parsed: %q", id.UnvalidatedInput, id.AsURI()))
@@ -154,6 +162,8 @@ func (id ActorID) Validate() []string {
 
 func (id PersonID) Validate() []string {
 	result := id.ActorID.Validate()
+	result = append(result, validation.ValidateNotEmpty(id.Source, "source")...)
+	result = append(result, validation.ValidateOneOf(id.Source, []string{"forgejo", "gitea"})...)
 	switch id.Source {
 	case "forgejo", "gitea":
 		if strings.ToLower(id.Path) != "api/v1/activitypub/user-id" && strings.ToLower(id.Path) != "api/activitypub/user-id" {
@@ -165,6 +175,8 @@ func (id PersonID) Validate() []string {
 
 func (id RepositoryID) Validate() []string {
 	result := id.ActorID.Validate()
+	result = append(result, validation.ValidateNotEmpty(id.Source, "source")...)
+	result = append(result, validation.ValidateOneOf(id.Source, []string{"forgejo", "gitea"})...)
 	switch id.Source {
 	case "forgejo", "gitea":
 		if strings.ToLower(id.Path) != "api/v1/activitypub/repository-id" && strings.ToLower(id.Path) != "api/activitypub/repository-id" {
@@ -192,31 +204,3 @@ func removeEmptyStrings(ls []string) []string {
 	}
 	return rs
 }
-
-func IsValid[T Validateables](value T) (bool, error) {
-	if err := value.Validate(); len(err) > 0 {
-		errString := strings.Join(err, "\n")
-		return false, fmt.Errorf(errString)
-	}
-	return true, nil
-}
-
-/*
-func (a RepositoryId) IsValid() (bool, error) {
-	if err := a.Validate(); len(err) > 0 {
-		errString := strings.Join(err, "\n")
-		return false, fmt.Errorf(errString)
-	}
-
-	return true, nil
-}
-
-func (a PersonId) IsValid() (bool, error) {
-	if err := a.Validate(); len(err) > 0 {
-		errString := strings.Join(err, "\n")
-		return false, fmt.Errorf(errString)
-	}
-
-	return true, nil
-}
-*/
diff --git a/models/forgefed/nodeinfo.go b/models/forgefed/nodeinfo.go
new file mode 100644
index 0000000000..fb1e5d35da
--- /dev/null
+++ b/models/forgefed/nodeinfo.go
@@ -0,0 +1,78 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgefed
+
+import (
+	"net/url"
+
+	"code.gitea.io/gitea/modules/validation"
+	"github.com/valyala/fastjson"
+)
+
+type (
+	SourceType string
+)
+
+type SourceTypes []SourceType
+
+const (
+	ForgejoSourceType SourceType = "frogejo"
+)
+
+var KnownSourceTypes = SourceTypes{
+	ForgejoSourceType,
+}
+
+// NodeInfo data type
+// swagger:model
+type NodeInfoWellKnown struct {
+	Href string
+}
+
+func NodeInfoWellKnownUnmarshalJSON(data []byte) (NodeInfoWellKnown, error) {
+	p := fastjson.Parser{}
+	val, err := p.ParseBytes(data)
+	if err != nil {
+		return NodeInfoWellKnown{}, err
+	}
+	href := string(val.GetStringBytes("links", "0", "href"))
+	return NodeInfoWellKnown{Href: href}, nil
+}
+
+func NewNodeInfoWellKnown(body []byte) (NodeInfoWellKnown, error) {
+	result, err := NodeInfoWellKnownUnmarshalJSON(body)
+	if err != nil {
+		return NodeInfoWellKnown{}, err
+	}
+
+	if valid, outcome := validation.IsValid(result); !valid {
+		return NodeInfoWellKnown{}, outcome
+	}
+
+	return NodeInfoWellKnown{}, nil
+}
+
+// Validate collects error strings in a slice and returns this
+func (node NodeInfoWellKnown) Validate() []string {
+	var result []string
+	result = append(result, validation.ValidateNotEmpty(node.Href, "Href")...)
+
+	parsedUrl, err := url.Parse(node.Href)
+	if err != nil {
+		result = append(result, err.Error())
+		return result
+	}
+
+	if parsedUrl.Host == "" {
+		result = append(result, "Href has to be absolute")
+	}
+
+	result = append(result, validation.ValidateOneOf(parsedUrl.Scheme, []string{"http", "https"})...)
+
+	if parsedUrl.RawQuery != "" {
+		result = append(result, "Href may not contain query")
+	}
+
+	return result
+}
diff --git a/models/forgefed/nodeinfo_test.go b/models/forgefed/nodeinfo_test.go
new file mode 100644
index 0000000000..0df7b905b5
--- /dev/null
+++ b/models/forgefed/nodeinfo_test.go
@@ -0,0 +1,67 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgefed
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+
+	"code.gitea.io/gitea/modules/validation"
+)
+
+func Test_NodeInfoWellKnownUnmarshalJSON(t *testing.T) {
+	type testPair struct {
+		item    []byte
+		want    NodeInfoWellKnown
+		wantErr error
+	}
+
+	tests := map[string]testPair{
+		"with href": {
+			item: []byte(`{"links":[{"href":"https://federated-repo.prod.meissa.de/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`),
+			want: NodeInfoWellKnown{
+				Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo",
+			},
+		},
+		"empty": {
+			item:    []byte(``),
+			wantErr: fmt.Errorf("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""),
+		},
+		// "with too long href": {
+		// 	item:    []byte(`{"links":[{"href":"https://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`),
+		// 	wantErr: fmt.Errorf("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""),
+		// },
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			got, err := NodeInfoWellKnownUnmarshalJSON(tt.item)
+			if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
+				t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("UnmarshalJSON() got = %q, want %q", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_NodeInfoWellKnownValidate(t *testing.T) {
+	sut := NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo"}
+	if b, err := validation.IsValid(sut); !b {
+		t.Errorf("sut should be valid, %v, %v", sut, err)
+	}
+
+	sut = NodeInfoWellKnown{Href: "./federated-repo.prod.meissa.de/api/v1/nodeinfo"}
+	if _, err := validation.IsValid(sut); err.Error() != "Href has to be absolute\nValue  is not contained in allowed values [[http https]]" {
+		t.Errorf("validation error expected but was: %v\n", err)
+	}
+
+	sut = NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo?alert=1"}
+	if _, err := validation.IsValid(sut); err.Error() != "Href may not contain query" {
+		t.Errorf("sut should be valid, %v, %v", sut, err)
+	}
+}
diff --git a/models/forgefed/star.go b/models/forgefed/star.go
index 7e38b07c2c..21f44361ef 100644
--- a/models/forgefed/star.go
+++ b/models/forgefed/star.go
@@ -8,24 +8,10 @@ import (
 	"github.com/valyala/fastjson"
 )
 
-type (
-	SourceType string
-)
-
-type SourceTypes []SourceType
-
 const (
 	StarType ap.ActivityVocabularyType = "Star"
 )
 
-const (
-	ForgejoSourceType SourceType = "frogejo"
-)
-
-var KnownSourceTypes = SourceTypes{
-	ForgejoSourceType,
-}
-
 // Star activity data type
 // swagger:model
 type Star struct {
diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go
index 4828b60cc5..006f48d429 100644
--- a/routers/api/v1/activitypub/repository.go
+++ b/routers/api/v1/activitypub/repository.go
@@ -90,6 +90,10 @@ func RepositoryInbox(ctx *context.APIContext) {
 	log.Info("RepositoryInbox: activity:%v", activity)
 
 	// parse actorID (person)
+	// rawActorID, err := forgefed.NewActorID(activity.Actor.GetID().String())
+
+	// nodeInfo, err := createNodeInfo(rawActorID)
+
 	actorID, err := forgefed.NewPersonID(activity.Actor.GetID().String(), string(activity.Source))
 	if err != nil {
 		ctx.ServerError("Validate actorId", err)