diff --git a/mateclaw-server/src/main/java/vip/mate/auth/pat/PersonalAccessTokenController.java b/mateclaw-server/src/main/java/vip/mate/auth/pat/PersonalAccessTokenController.java
new file mode 100644
index 00000000..b945f455
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/auth/pat/PersonalAccessTokenController.java
@@ -0,0 +1,88 @@
+package vip.mate.auth.pat;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import vip.mate.auth.model.UserEntity;
+import vip.mate.auth.service.AuthService;
+import vip.mate.common.result.R;
+import vip.mate.exception.MateClawException;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RFC-03 Lane I1 — Personal Access Token CRUD endpoints.
+ *
+ *
Authenticated callers (JWT or another PAT) manage their own tokens
+ * here. Cross-user access is impossible: every query is scoped to
+ * {@code Authentication.getName()} server-side, so tampering with the
+ * {@code X-User-Id} header has no effect.
+ *
+ *
Plaintext is returned exactly once on {@link #create}; subsequent
+ * lookups expose only metadata (id, name, scopes, last_used_at,
+ * expires_at). The DB never stores plaintext at any point.
+ */
+@Tag(name = "Personal Access Tokens")
+@RestController
+@RequestMapping("/api/v1/auth/tokens")
+@RequiredArgsConstructor
+public class PersonalAccessTokenController {
+
+ private final PersonalAccessTokenService patService;
+ private final AuthService authService;
+
+ @Operation(summary = "List my PATs (metadata only — plaintext is never returned after creation)")
+ @GetMapping
+ public R> list(Authentication auth) {
+ UserEntity user = requireUser(auth);
+ return R.ok(patService.listByUser(user.getId()));
+ }
+
+ @Operation(summary = "Mint a new PAT — returned plaintext is shown once and cannot be recovered")
+ @PostMapping
+ public R