Quantum Computing: A Complete Learning Path

QUANTUM SERIES 2026
The complete learning path, from a single qubit to Grover’s algorithm.

This is the index to the Techucation Quantum Series: a sequence of hands-on posts that build quantum computing from the ground up. Each one stands on its own, but together they form a single arc, from what a qubit actually is, through the rules that make quantum mechanics strange, to the algorithms that turn those rules into a real speedup. The order below is a learning path from first principles to algorithms, not the order the posts were written.

New to the topic? Read top to bottom. Already comfortable with qubits and gates? Skip ahead to the algorithms in Section 4.

1  ·  Start Here: Qubits and Superposition
1
Introduction to Quantum Computing: Qubits, Hadamard Gates, and Superposition
First principles: what a qubit is, how the Hadamard gate builds superposition, and why tensor products scale the state space.
2  ·  The Hadamard Toolkit
2
The Quantum Fourier Transform of a Single Qubit is the Hadamard Transform
The one-qubit QFT turns out to be exactly the Hadamard gate, a small result that anchors the bigger picture.
3
The Walsh-Hadamard Matrix: Backbone of Grover’s Diffusion Operator
How the Hadamard sign table generalises to n qubits and powers Grover’s diffusion step.
3  ·  The Rules of the Quantum World
4
Reversible Computation in Quantum Computing
Why every quantum gate must be reversible, and how ancilla bits turn irreversible logic into unitary logic.
5
The Cost of Garbage in Quantum Computing
Leftover junk qubits destroy interference; uncomputation cleans them up to protect the speedup.
6
The No-Cloning Theorem: Why You Cannot Copy a Qubit
A short proof that an unknown quantum state cannot be duplicated, and what that impossibility makes possible.
7
Quantum Teleportation, and Why It Is Not Cloning
Moving an unknown qubit from one place to another without ever copying it, using entanglement and two classical bits.
4  ·  Algorithms
8
Understanding Phase Kickback
The mechanism where the target qubit flips the control’s phase, the trick underneath Deutsch’s, Grover’s, and Shor’s.
9
Deutsch’s Algorithm: The Four Cases
The four one-bit Boolean functions, the reversible oracle, and the single query that beats the classical two.
10
Deutsch Revisited: Quantum vs Classical in Qiskit
The same algorithm in running Qiskit code: two classical queries against one quantum query.
11
Grover’s Algorithm: Inversion About the Mean
A full three-qubit walkthrough of the oracle and the amplitude amplification that surfaces the marked item.
5  ·  Entanglement and Bell’s Inequality
12
The CHSH Game Simulator and Bell’s Inequality
An interactive game where quantum entanglement beats the classical 75 percent ceiling and violates Bell’s inequality.

Quantum Series 2026  ·  Built with Qiskit 1.x

✦ This article was generated with the assistance of Claude by Anthropic

Quantum Teleportation in Quantum Computing, and Why It Isn’t Cloning

QUANTUM SERIES 2026
Moving a qubit you are forbidden to copy.

Teleportation is the most over-sold word in quantum computing. It conjures Star Trek transporters and faster-than-light messaging, and almost every popular account quietly implies you end up with a copy of the original. You do not. Quantum teleportation moves an unknown quantum state from one qubit to another while destroying the original, and that destruction is not an incidental detail. It is the mechanism that keeps the protocol on the right side of the no-cloning theorem.


1  ·  A quick word on no-cloning

This whole protocol is haunted by the no-cloning theorem: there is no operation that copies an arbitrary unknown quantum state. That proof and its consequences are covered in a recent post, The No-Cloning Theorem in Quantum Computing: Why You Can’t Copy a Qubit, and are not re-derived here.

The single fact needed going forward: you cannot deterministically duplicate an unknown qubit. The qualifiers matter, because teleportation lives in the gaps between them. The theorem forbids copying unknown states (a known state you can re-prepare at will), it forbids deterministic, perfect copies, and it is a statement about unitary operations — and measurement, which teleportation leans on at the decisive moment, is not unitary.

2  ·  Why teleportation is not cloning

Here is the distinction in one sentence: cloning would leave |ψ⟩ in two places; teleportation leaves it in exactly one.

Partway through the protocol, Alice’s message qubit is measured. Measurement collapses it into a classical basis state — a plain |0⟩ or |1⟩ carrying none of the original amplitudes. By the time Bob’s qubit holds |ψ⟩, Alice’s qubit demonstrably does not. The state was relocated, and the accounting is exact: one copy in, one copy out. No moment ever exists where two qubits both carry |ψ⟩, so there is nothing for the no-cloning theorem to object to.

This also kills the faster-than-light fantasy. The protocol forces Alice to send Bob two ordinary classical bits. Until they arrive, Bob’s qubit is information-free, as the algebra below shows.

3  ·  The complete circuit

Here is the whole protocol in a single circuit. Read it left to right; the amber dashed dividers mark the three stages. The message qubit q0 starts in the unknown state |ψ⟩, while q1 and q2 both start in |0⟩.

1. Bell pair2. Alice measures3. Bob corrects
q0:HM
q1:H+M
q2:+XZ

Stage 1 — Bell pair. Alice and Bob share an entangled pair: an H on q1 followed by a CNOT from q1 onto q2 prepares (|00⟩ + |11⟩)/√2 across the two halves.

Stage 2 — Alice measures. Alice folds her message into the pair (a CNOT from q0 onto q1, then an H on q0) and measures both of her qubits, collapsing them to two classical bits.

Stage 3 — Bob corrects. Depending on those two bits, Bob applies an X and/or a Z to q2 — the X controlled by Alice’s q1 bit, the Z by her q0 bit — and q2 emerges as |ψ⟩. The next section is the algebra that fixes exactly which correction each outcome needs.

4  ·  The protocol in bra-ket and tensor form

Now the algebra behind that circuit. Three qubits: q0 carries |ψ⟩ = α|0⟩ + β|1⟩ (Alice), q1 is Alice’s half of the Bell pair, and q2 is Bob’s half. They pre-share |Φ+⟩ = (|00⟩ + |11⟩)/√2 on q1 q2.

Step 1 — the starting state. Tensor the message against the Bell pair:

|ψ⟩ = α|0⟩ + β|1⟩            (the unknown message on q0)

|ψ⟩ ⊗ |Φ+⟩ = (α|0⟩ + β|1⟩) ⊗ (|00⟩ + |11⟩)/√2
           = (1/√2) [ α|000⟩ + α|011⟩ + β|100⟩ + β|111⟩ ]

Step 2 — Alice’s CNOT (control q0, target q1):

CNOT (control q0, target q1)  flips q1 wherever q0 = 1:

= (1/√2) [ α|000⟩ + α|011⟩ + β|110⟩ + β|101⟩ ]

Step 3 — Alice’s Hadamard on q0. Substitute |0⟩ → (|0⟩+|1⟩)/√2 and |1⟩ → (|0⟩−|1⟩)/√2, then collect by the value of (q0 q1):

H on q0, then collect by the pair Alice will measure (q0 q1):

= (1/2) [ |00⟩ (α|0⟩ + β|1⟩)    ← case A
        + |01⟩ (α|1⟩ + β|0⟩)    ← case B
        + |10⟩ (α|0⟩ − β|1⟩)    ← case C
        + |11⟩ (α|1⟩ − β|0⟩) ]  ← case D

Each bracketed q2 state is the original |ψ⟩ acted on by a known Pauli. Alice measures, gets one of four outcomes (each with probability 1/4), sends the two bits to Bob, and Bob undoes the Pauli. The Case column ties each row back to the matching line in Step 3:

Case
Alice measures (q0 q1)
Bob holds on q2
Relation to |ψ⟩
Bob applies
A
00
α|0⟩ + β|1⟩
I |ψ⟩
nothing
B
01
α|1⟩ + β|0⟩
X |ψ⟩
X
C
10
α|0⟩ − β|1⟩
Z |ψ⟩
Z
D
11
α|1⟩ − β|0⟩
ZX |ψ⟩
X then Z

Bob applies Zm0 Xm1 — an X if m1 = 1, then a Z if m0 = 1 — and every branch lands back on α|0⟩ + β|1⟩ = |ψ⟩.

5  ·  Qiskit — classical feed-forward

This version measures mid-circuit and uses real classical conditioning via if_test, the honest picture of the protocol. The verification trick: rather than read out Bob’s state, apply the inverse of the preparation to q2. If teleportation worked, that must collapse q2 to |0⟩ on every shot.

import numpy as np
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit.circuit.library import StatePreparation
from qiskit.quantum_info import random_statevector
from qiskit_aer import AerSimulator

# Unknown state to teleport (StatePreparation is unitary -> invertible)
psi  = random_statevector(2)
prep = StatePreparation(psi)

q   = QuantumRegister(3, “q”)     # q0 message, q1 Alice, q2 Bob
mz  = ClassicalRegister(1, “mz”)  # q0 result -> drives Z
mx  = ClassicalRegister(1, “mx”)  # q1 result -> drives X
out = ClassicalRegister(1, “out”) # verification bit
qc  = QuantumCircuit(q, mz, mx, out)

qc.append(prep, [0]); qc.barrier()        # 1. load |ψ⟩ onto q0
qc.h(1); qc.cx(1, 2); qc.barrier()        # 2. Bell pair on (q1,q2)
qc.cx(0, 1); qc.h(0)                      # 3. Alice’s basis change
qc.measure(0, mz); qc.measure(1, mx); qc.barrier()

with qc.if_test((mx, 1)):                 # 4. Bob’s corrections
    qc.x(2)
with qc.if_test((mz, 1)):
    qc.z(2)
qc.barrier()

qc.append(prep.inverse(), [2])            # 5. un-prepare on Bob: must read 0
qc.measure(2, out)

counts = AerSimulator().run(transpile(qc, AerSimulator()), shots=4000).result().get_counts()
clean  = all(k.split()[0] == “0” for k in counts)   # leftmost bit = out
print(counts); print(“Teleportation verified:”, clean)
The out bit comes back 0 on 100% of shots regardless of the random (mx, mz) branch — exactly the claim that Bob reconstructs |ψ⟩ in every case.

6  ·  No faster-than-light, and the takeaway

Before Bob learns (m0, m1), his qubit is an equal mixture of the four branch states. Averaging the four projectors gives the Pauli twirl:

ρ_Bob = (1/4)( |ψ⟩⟨ψ| + X|ψ⟩⟨ψ|X
              + Z|ψ⟩⟨ψ|Z + XZ|ψ⟩⟨ψ|ZX )  =  I/2   for every |ψ⟩

for any |ψ⟩. Bob’s local state is identical no matter what Alice sent, so no information has reached him yet. The classical bits are not a formality — they are the only thing that carries the state across, and they travel no faster than light.

Takeaway. No-cloning forbids duplicating an unknown qubit (proof here). Teleportation never attempts a copy: it entangles the message with a shared Bell pair, measures the original out of existence, and ships two classical bits naming which of four Pauli corrections rebuilds the state on the other end. Exactly one copy before, exactly one after. No-cloning is not a bug the protocol works around — it is the reason the protocol has to look the way it does.

Quantum Series 2026  ·  Built with Qiskit 1.x

✦ This article was generated with the assistance of Claude by Anthropic

Move Your WordPress.com Domain to Cloudflare and Halve the Renewal

TECHUCATION  ·  DOMAINS 2026
Moving a WordPress.com domain to Cloudflare Registrar, and what it actually costs.

My WordPress.com domain renewal notice came in at £16/year, with auto-renew switched off and the expiry only a couple of days away. That is a fine price for convenience, but the same name sits on Cloudflare Registrar at wholesale cost with no markup. This post walks through the move end to end, including the one step that quietly breaks things if you rush it, and lays out the real cost difference.

The short version: you do not “renew at Cloudflare”. You transfer the domain to Cloudflare, which extends the registration by a year in the process. The saving is real, but the order of operations matters.


1  ·  The cost case

Cloudflare Registrar charges exactly what the registry charges them, with zero markup, and bundles WHOIS privacy for free. Retail registrars add a margin on top. For a .net the difference looks like this:

Item WordPress.com Cloudflare Registrar
.net renewal (1 yr) £16 (approx US$20) approx US$10.44 (at cost)
Markup Retail margin None (wholesale pass-through)
WHOIS privacy Included Included, free
Approx 5-year cost approx US$100 approx US$52
Figures are indicative for mid-2026. Because Cloudflare is at cost, your renewal tracks the registry wholesale rate, so it moves up if the registry (Verisign for .net) raises prices. The trade-off: Cloudflare requires the domain to run on Cloudflare DNS.
2  ·  The gotcha: transfer, not renew, and DNS moves first

Two things trip people up:

You cannot renew a domain at a registrar where it is not registered. To get Cloudflare pricing you transfer the domain in. A gTLD transfer (.com, .net, .org) automatically adds one year, so it replaces the renewal rather than stacking on top of it.

Cloudflare will not let you transfer the registration until the domain is already running on Cloudflare DNS. So the real sequence is DNS first, registrar second, with any redirect rebuilt in the middle so the site never goes dark.

Mind the expiry window. Do not start this within a few days of expiry and then walk away. A transfer left to auto-complete can take up to 5 days. If you are close to the date, expedite it (see step 6) rather than waiting out the clock.
3  ·  Step by step

The full order of operations:

1   Unlock the domain at WordPress.com, get the EPP / auth code
2   Add the domain as a Free zone in Cloudflare
3   Stage the redirect in Cloudflare (before flipping nameservers)
4   Change nameservers at WordPress.com to Cloudflare’s pair
5   Wait for the Cloudflare zone to go Active
6   Initiate the transfer in Cloudflare, paste the EPP code, pay
7   Approve / expedite the transfer on the WordPress.com side
8   Verify: registrar, new expiry, redirect, DNSSEC

Step 1. In WordPress.com under Upgrades > Domains, open the domain, turn off Transfer lock, then start the transfer-out flow far enough to reveal the EPP / auth code. Copy it. Do not let WordPress walk you all the way through its own wizard, you only want the code.

Step 2. In the Cloudflare dashboard, Add a domain, choose the Free plan. Cloudflare scans existing DNS and assigns you two nameservers like name.ns.cloudflare.com. Note them.

Step 3. If the domain is a redirect (common when it just points at another site of yours), rebuild that redirect in Cloudflare now while it sits idle. See section 4.

Step 4. Back at WordPress.com, switch the nameservers from WordPress to the two Cloudflare nameservers.

Step 5. Cloudflare flips the zone to Active once it sees the nameserver change, usually under an hour. The transfer option stays greyed out until then.

Step 6. Once Active, go to Domain Registration > Transfer Domains, select the domain, paste the EPP code, and pay the at-cost fee. This payment includes the added year.

Step 7. Watch for the confirmation from the losing registrar. WordPress.com surfaces a Transfer Management page with an Accept Transfer button. Clicking it skips the 5-day wait and completes the transfer in minutes.

Done. The registry should now show the registrar as Cloudflare, status Active, and the expiry rolled forward by a year.
4  ·  The redirect-only trap

If your domain only exists to redirect to another site, the imported records (the host’s A records and a couple of CNAMEs) become redundant once you build a Cloudflare Redirect Rule. Tempting to delete them all for a tidy zone. Here is the trap:

A Cloudflare Redirect Rule only fires on traffic that reaches Cloudflare’s edge. Traffic only reaches the edge if the hostname resolves to a proxied record (orange cloud). Delete every record and the name resolves to nothing, so the redirect goes dark.

The fix is to keep one proxied placeholder for the rule to attach to:

Type Name Value Proxy
A @ 192.0.2.1 Proxied
CNAME www @ Proxied (only if you want www)
TXT @ “google-site-verification=…” DNS only

192.0.2.1 is a reserved TEST-NET address that never routes anywhere, which is exactly what you want: the proxy intercepts the request before that IP is ever used. Then a single Redirect Rule (Rules > Redirect Rules) handles the rest:

Field Setting
When (match) hostname ends with example.net
Then (action) 301 to https://example.com + path
Preserve query string On

Keep the TXT verification record as DNS only. It does not need to resolve to anything, it just needs to exist.

5  ·  After the transfer

Three things to settle once the registrar shows Cloudflare:

Auto-renew. Optional, but turning it on (Domain Registration > your domain) means you never repeat the two-days-from-expiry scramble. If you leave it off, set yourself a reminder a few weeks before the date.

DNSSEC. Worth enabling for the added integrity protection. Because Cloudflare is now both your DNS and your registrar, the DS record is published to the registry automatically, so it activates without you touching the registrar by hand. Give it from a few minutes up to a couple of hours to show as signed at the registry.

Registrar lock. On by default at Cloudflare, so that one is already handled.

One caution with DNSSEC: once the DS record is published, do not move nameservers away from Cloudflare or disable DNSSEC out of order. Breaking the signature chain is the classic way to make a domain go dark, because validating resolvers will reject it. As long as DNS stays on Cloudflare, you are fine.

Net result: same domain, half the renewal cost, a year added, and the registrar consolidated with your DNS. The only part that needs care is the DNS-first ordering and not over-deleting the redirect records. Everything else is a few clicks and a short wait.


Techucation 2026  ·  Domains & DevOps  ·  Cloudflare Registrar + WordPress.com

✦ This article was generated with the assistance of Claude by Anthropic

The No-Cloning Theorem in Quantum Computing: Why You Can’t Copy a Qubit

QUANTUM SERIES 2026
The no-cloning theorem: why an unknown quantum state cannot be copied, and what that buys us.

Copying is so ordinary in classical computing that we never think about it. Every assignment, every memory write, every Ctrl+C duplicates a bit string perfectly and at will. Quantum mechanics flatly refuses to do the same for an unknown state. There is no machine, no clever circuit, no sequence of gates that takes an arbitrary qubit and hands you back two identical copies of it. This is the no-cloning theorem, and far from being a limitation we tolerate, it is the structural reason quantum key distribution is secure, the reason entanglement cannot be used to signal faster than light, and the reason quantum error correction had to be reinvented from scratch.

The proof is short. It falls out of two properties we have already relied on throughout this series: linearity of quantum evolution, and unitarity. Below we prove it both ways, clear up the very common confusion about CNOT “copying” a qubit, walk through the consequences, and then watch a cloning attempt fail in Qiskit.


1  ·  What “cloning” would actually mean

A cloning machine would be a single fixed unitary U together with a standard blank register |e⟩, such that for every possible input state |ψ⟩ it produces:

U  ( |ψ⟩ ⊗ |e⟩ )  =  |ψ⟩ ⊗ |ψ⟩

Three pieces of that statement deserve unpacking, because the whole theorem turns on them.

What |e⟩ is, and what “blank” means. The register |e⟩ is simply the second slot, the one that will end up holding the copy. We call it “blank” by analogy with an empty sheet of paper or an uninitialised memory cell: before the operation it carries no information whatsoever about the input |ψ⟩. The conventional choice is |0⟩, or |0…0⟩ when the copy needs several qubits, but |e⟩ does not have to be the zero vector specifically. Any fixed pure state works equally well for the theorem. The property that actually matters is not its literal value but that it is independent of the unknown input. A register that already secretly equalled |ψ⟩ would not be blank, and you would not have copied anything, you would have been handed the answer.

What “fixed” means, for both U and |e⟩. Fixed means chosen in advance and identical on every run, never allowed to depend on which |ψ⟩ happens to come in. This is the condition that keeps the problem honest. If you were permitted to pick |e⟩ or U based on the input, you would first have to know what |ψ⟩ is, and at that point there is nothing left to clone: you could just prepare a second copy yourself. So the cloner gets one unitary and one starting register, and they must work for an arbitrary, unknown input. The claim of the theorem is that no such pair (U, |e⟩) exists.

Why cloning a known state is allowed, and how you do it. The theorem forbids copying an unknown arbitrary state. It says nothing about states you already understand, and there are two everyday ways such a state can be “known”:

You know… How you get two copies
The preparation recipe (the amplitudes, or the circuit that builds it) Just run the preparation twice on two fresh |0⟩ qubits. You are re-preparing from a known description, not copying an unknown state.
That the state is one of a known orthogonal set Measure in the basis that distinguishes the set. Orthogonal states are eigenstates of that measurement, so they survive it undisturbed. Read off the label, then re-prepare as many copies as you like.

As a concrete example of the first route, if you are told the state is |ψ⟩ = Ry(θ)|0⟩ for a known angle θ, you simply apply Ry(θ) to two separate qubits. No universal device is involved, and no rule is broken. The contradiction in the next two sections appears only when no such side information exists, that is, when the same machine must handle a continuum of unknown inputs at once.

The one-line distinction: you can always re-prepare a state you know how to build, and you can copy states that act like classical labels. What you cannot do is build one fixed machine that duplicates an arbitrary, unknown state handed to you blind.

2  ·  Proof I: linearity forbids it

Assume, for contradiction, that a universal cloner U exists. Pick two arbitrary states |ψ⟩ and |φ⟩. By assumption U clones both of them:

U |ψ⟩|e⟩  =  |ψ⟩|ψ⟩
U |φ⟩|e⟩  =  |φ⟩|φ⟩

Now feed the machine a superposition of the two inputs, |χ⟩ = (1/√2)(|ψ⟩ + |φ⟩). Quantum evolution is linear, so U acts term by term on the input:

U |χ⟩|e⟩  =  (1/√2) ( U|ψ⟩|e⟩  +  U|φ⟩|e⟩ )
         =  (1/√2) ( |ψ⟩|ψ⟩  +  |φ⟩|φ⟩ )

But if U is genuinely a cloner, then by definition it must also clone |χ⟩ itself, producing |χ⟩|χ⟩. Expanding that out:

|χ⟩|χ⟩  =  (1/2) ( |ψ⟩ + |φ⟩ )( |ψ⟩ + |φ⟩ )
      =  (1/2) ( |ψ⟩|ψ⟩ + |ψ⟩|φ⟩ + |φ⟩|ψ⟩ + |φ⟩|φ⟩ )

The two results disagree. Linearity gave us only the “diagonal” terms |ψ⟩|ψ⟩ and |φ⟩|φ⟩, whereas true cloning of |χ⟩ demands the cross terms |ψ⟩|φ⟩ and |φ⟩|ψ⟩ as well. They can only match if the cross terms vanish, which happens precisely when |ψ⟩ and |φ⟩ are identical or orthogonal. For general, non-orthogonal states the equality is impossible. The cloner cannot exist.

The contradiction is the same one that powers most quantum no-go results: a device defined by its action on a basis is forced, by linearity, into a behaviour on superpositions that contradicts the original specification.

3  ·  Proof II: unitarity forbids it

The linearity argument shows cloning fails for superpositions. A second proof, often considered the cleaner one, uses the fact that unitary operations preserve inner products. Suppose again that U clones two states:

U |ψ⟩|e⟩  =  |ψ⟩|ψ⟩
U |φ⟩|e⟩  =  |φ⟩|φ⟩

The inner product is taken over the full two-register states that U acts on, not over |ψ⟩ and |φ⟩ on their own. Label the two inputs and their images under U:

Input A   = |ψ⟩|e⟩        Output UA  = |ψ⟩|ψ⟩
Input B   = |φ⟩|e⟩        Output UB  = |φ⟩|φ⟩

If U is unitary, then   ⟨A|B⟩  =  ⟨UA|UB⟩

That identity, ⟨A|B⟩ = ⟨UA|UB⟩, is the definition of a unitary: it preserves inner products. U never acts on the inner product itself, which is only a number; it acts on the states, and unitarity says doing so cannot change their overlap. Now evaluate both sides, using the tensor-product factorisation ⟨a|⟨b| · |c⟩|d⟩ = ⟨a|c⟩ ⟨b|d⟩ and ⟨e|e⟩ = 1:

⟨A|B⟩   = ⟨ψ|⟨e| · |φ⟩|e⟩  =  ⟨ψ|φ⟩ ⟨e|e⟩  =  ⟨ψ|φ⟩
⟨UA|UB⟩ = ⟨ψ|⟨ψ| · |φ⟩|φ⟩  =  ⟨ψ|φ⟩ ⟨ψ|φ⟩  =  ⟨ψ|φ⟩²

so   ⟨ψ|φ⟩  =  ⟨ψ|φ⟩²

Write x = ⟨ψ|φ⟩. The condition x = x² rearranges to x(x − 1) = 0, whose only solutions are x = 0 or x = 1. In other words, cloning is consistent only when the two states are orthogonal (x = 0) or identical (x = 1). Any pair of distinct, non-orthogonal states, the generic case, breaks the equality. No universal cloner can exist.

⟨ψ|φ⟩ Relationship Cloneable?
0 Orthogonal (a known basis set) Yes, this is just classical copying
1 Identical (already known) Yes, trivially
anything else Distinct, non-orthogonal No

Both proofs converge on the same boundary. Orthogonal states carry distinguishable classical labels and can be copied; overlapping quantum states cannot.

4  ·  “But doesn’t CNOT copy a qubit?”

This is the single most common point of confusion, and it is worth resolving carefully. A CNOT with the input qubit as control and a fresh |0⟩ as target looks exactly like a copy operation. On computational basis states it behaves like one:

CNOT |0⟩|0⟩  =  |0⟩|0⟩
CNOT |1⟩|0⟩  =  |1⟩|1⟩

So far it really is copying: the second qubit ends up matching the first. The trouble appears the moment the control is a superposition. Take |ψ⟩ = a|0⟩ + b|1⟩ as control and |0⟩ as target:

CNOT ( a|0⟩ + b|1⟩ )|0⟩  =  a|00⟩ + b|11⟩

Compare that with what a true clone would have produced, namely |ψ⟩|ψ⟩:

|ψ⟩|ψ⟩  =  a²|00⟩ + ab|01⟩ + ab|10⟩ + b²|11⟩

These are different states. The CNOT output a|00⟩ + b|11⟩ is an entangled pair, not two independent copies. If you discard the original and look at the “copy” on its own, you do not find |ψ⟩; you find a mixed state that has lost the phase relationship between a and b entirely. CNOT copies the value in the computational basis but destroys the superposition structure that made the state quantum in the first place. That is exactly the gap between copying classical information, which is always allowed, and cloning a quantum state, which is not.

Rule of thumb: CNOT copies basis labels, not quantum states. The instant the input is in superposition, the “copy” is entanglement, not duplication.

5  ·  Why this single theorem matters so much

No-cloning is not a footnote. Three of the most important results in quantum information rest directly on it.

Quantum key distribution. In BB84, Alice sends qubits encoded in randomly chosen bases. An eavesdropper, Eve, would love to intercept each qubit, keep a copy, and forward the original untouched so she can measure her copies later once the bases are announced. No-cloning makes that impossible. Eve cannot duplicate an unknown qubit, so she is forced to measure in transit, and measuring in the wrong basis disturbs the state and injects a detectable error rate. The security of the protocol is the no-cloning theorem wearing a different hat.

No faster-than-light signalling. Entanglement correlates distant measurement outcomes, which has always tempted people into superluminal-communication schemes. Many of those schemes secretly assume Bob can clone his half of an entangled pair, make many copies, and read off the statistics to learn which basis Alice measured in. No-cloning closes that loophole and is one of the reasons quantum mechanics and relativity coexist without paradox.

Quantum error correction. Classical error correction leans on the simplest trick imaginable: copy the bit several times and take a majority vote. That is illegal in the quantum setting, because you cannot copy an unknown logical state. Quantum codes had to be built on a different principle, spreading logical information across many physical qubits using entanglement and measuring error syndromes without ever reading or duplicating the encoded state. The whole architecture of fault tolerance is shaped by the fact that the obvious classical fix is off the table.

A useful way to hold it in your head: no-cloning is what stops quantum information from behaving like classical information. Almost everything that makes quantum protocols both powerful and secure traces back to that one restriction.

6  ·  Watching a clone fail in Qiskit

The abstract proof is convincing, but it is satisfying to watch the failure happen numerically. We attempt the naive CNOT “clone” of the state |+⟩ = H|0⟩, then measure how badly it falls short by two yardsticks: fidelity against the intended |+⟩|+⟩, and the purity of the copy qubit on its own.

Three terms in plain language:

Fidelity is a similarity score between two quantum states, from 0 to 1. Think of it as a match percentage: 1.0 means identical, 0 means completely unrelated.

Reduced state is the honest description of one qubit when you ignore its partner. Picture two coins glued so they always land the same way: cover one and look at just the other, and that single coin looks like a plain 50/50 flip. The “match” information lived in the pair, not in either coin alone. Discarding the partner mathematically is called tracing it out.

Purity tells you whether a state is sharp and definite or a blurry random mixture. Purity 1 is a clean, definite state. For a single qubit the lowest value is 0.5, called maximally mixed, which is total randomness, the quantum version of a fair coin holding no information.

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, partial_trace, state_fidelity

qc = QuantumCircuit(2)
qc.h(0)         # prepare |+> on qubit 0 (the “original”)
qc.cx(0, 1)    # naive “cloning” attempt via CNOT

state = Statevector(qc)
print(state)   # (|00> + |11>)/sqrt(2), a Bell state

target = Statevector.from_label(‘++’)   # what we wanted: |+>|+>
print(state_fidelity(state, target))   # 0.5

rho_copy = partial_trace(state, [0])   # trace out the original
print(rho_copy.data)   # I/2, maximally mixed
print(rho_copy.purity().real)   # 0.5

Running it gives:

Output state:  (√2/2)|00⟩ + (√2/2)|11⟩
Fidelity to |+⟩|+⟩:  0.5
Reduced state of copy:  [[0.5, 0], [0, 0.5]]
Purity of copy:  0.5

Every number tells the same story. The output is a maximally entangled Bell state, not the product |+⟩|+⟩ we were hoping for, and the fidelity to the intended clone is only 0.5. Trace out the original and the supposed “copy” is the maximally mixed state I/2, with purity 0.5 rather than 1. The copy retains no memory of the |+⟩ superposition at all; it is as good as a fair coin. The CNOT did precisely what the theorem says it must: it produced correlation, not duplication.

Try changing qc.h(0) to any other single-qubit rotation, say qc.ry(0.7, 0). The fidelity stays stubbornly below 1 for every non-basis input, which is the no-cloning theorem reproduced one statevector at a time.

Quantum Series 2026  ·  Built with Qiskit 1.x

✦ This article was generated with the assistance of Claude by Anthropic

About the author

Malcolm Low is an Associate Professor at the Singapore Institute of Technology, writing on quantum computing, programming, and applied computing from Singapore.

Website: malcolmlow.com  ·  Singapore

凤凰传奇《山河图》MV

音乐分享 · MUSIC
凤凰传奇《山河图》— 一幅泼墨写就的锦绣河山

《山河图》(Shānhé Tú) 是凤凰传奇于 2019 年发布的一首国风作品。曲风磅礴大气,以”山””河””图”三段层层铺陈,把万壑千岩、星奔川鹜的意象堆叠成一幅徐徐展开的水墨长卷,再以泼墨收束。玲花高亢的唱腔与曾毅的念白对仗,配上民族器乐与现代编曲的融合,是凤凰传奇近年颇具代表性的一首作品。

开篇一段便奠定了整首歌的水墨基调:

看这山 万壑千岩 连一川又一川
让这河 星奔川鹜 结一湾又一湾
谱这图 鸾回凤舞 重峦高不可攀
泼了墨 墨饱笔酣 润我锦绣河山
— 凤凰传奇《山河图》开篇
完整歌词请在正版平台查看: KKBOX · QQ音乐 · Spotify

✦ This post was assembled with the assistance of Claude by Anthropic

Publishing to WordPress with Claude and the WordPress MCP Connector

AI Productivity · Claude · WordPress MCP

Publishing to WordPress with Claude and the WordPress MCP Connector

A practical guide to using Claude on claude.ai to draft, style, and publish blog posts directly to your WordPress site — with hard-won lessons on what WordPress strips, and how to work around it.

Several posts on this blog — including Word Embeddings Explained, SingIDBot, and the OpenClaw Telegram integration guide — were written, styled, and published entirely through Claude on claude.ai, using the WordPress MCP connector. This post documents exactly how that workflow operates, what problems arise, and how to resolve them.

The combination of Claude’s HTML generation capability and the WordPress MCP connector creates a surprisingly smooth authoring pipeline, provided you know the constraints WordPress imposes on custom HTML blocks. Those constraints are the non-obvious part, and they are addressed in detail below.

1. Prerequisites

Before you begin, the following need to be in place:

RequirementDetails
Claude account on claude.aiFree tier works; a Pro account gives longer contexts useful for large HTML posts.
WordPress.com siteThe MCP connector works with WordPress.com-hosted blogs. Self-hosted WordPress.org sites are not directly supported by the connector.
WordPress MCP connector enabledIn claude.ai, go to Settings → Integrations (or Connectors) and enable the WordPress.com connector. Authenticate with your WordPress.com account.

2. The Overall Workflow

The end-to-end process has three stages. Claude handles all of them in a single conversation.

Stage 1

Draft & Style

Provide Claude with your content and request a WordPress HTML block with inline CSS only. No <style> tags.

Stage 2

Review Output

Preview the rendered HTML in claude.ai artifacts. Request edits until the post looks correct.

Stage 3

Publish via MCP

Ask Claude to post using the WordPress connector. It checks categories and tags, creates any missing ones, then submits as a draft for your review.

The key prompt to kick off Stage 1:

“Generate a WordPress post using a custom HTML block with inline CSS only. Do not use <style> tags, <script> tags, or SVG.”

For Stage 3, once the HTML is finalised:

“Post this to malcolmlow.net using the WordPress connector. Use appropriate categories and tags. Post as a draft first.”

3. How the WordPress MCP Connector Works

The MCP (Model Context Protocol) connector gives Claude direct access to your WordPress.com REST API. When you ask Claude to post, it performs these operations internally:

  1. Loads the connector via an internal tool search, resolving the WordPress.com:wpcom-mcp-content-authoring tool.
  2. Lists existing categories using categories.list on your site to find IDs for any categories you want.
  3. Lists existing tags using tags.list to find or create the tags for the post.
  4. Creates missing categories or tags using categories.create / tags.create if they do not already exist on the site.
  5. Creates the post using posts.create, passing the HTML content, title, category IDs, tag IDs, and status (draft or publish).
  6. Returns the preview and edit URLs so you can open the draft in WordPress immediately.

The post content must be wrapped in a wp:html block comment:

<!– wp:html –>
  <div style=”…”>…post content…</div>
<!– /wp:html –>

4. Problems Encountered — and How They Were Solved

The workflow is productive but not without friction. Several issues emerged across multiple posts on this blog. Each is documented here with its root cause and fix.

Problem 1: WordPress strips <style> blocks entirely

What happened: Early posts used a <style> tag defining CSS classes. WordPress silently removed it on save, stripping all styling.

Fix: Every CSS rule must be written as an inline style="..." attribute. Claude handles this when instructed: “Use inline CSS only. Do not use <style> tags.”

Problem 2: WordPress strips SVG elements

What happened: Inline SVG circuit diagrams were stripped on save, leaving blank spaces.

Fix: Replace SVG with HTML-native layout (see Problems 6 and 7 for the full technique). Instruction: “Do not use SVG. Use CSS flexbox and inline-block spans for all diagrams.”

Problem 3: <script> tags are blocked

What happened: JavaScript interactivity was silently discarded by WordPress content sanitisation.

Fix: All interactivity must be replaced with pure HTML/CSS alternatives. Standing instruction: “No <script> tags.”

Problem 4: API parameter case sensitivity

What happened: Passing "order": "DESC" returned a validation error.

Fix: Always use lowercase: "order": "desc".

Problem 5: Duplicate categories and tags

What happened: Claude created duplicate taxonomy terms without checking first.

Fix: Always call categories.list and tags.list before any creation step.

Problem 6: Unicode box-drawing characters and &nbsp; spacing break alignment in WordPress

What happened: Unicode box-drawing characters inside <pre> elements and &nbsp; spacing in monospace divs looked correct in the claude.ai artifact but misaligned in WordPress — the theme’s font renders these characters at inconsistent widths.

Fix: Replace character-based layout with CSS layout:

  • Single-wire gates: display:flex; align-items:center rows with inline-block wire spans and bordered gate boxes.
  • Column vectors and matrices: display:flex container with border-left/border-right bracket spans and <br> for rows.

Instruction: “Never use &nbsp; for alignment or Unicode box-drawing characters. Use display:flex and inline-block spans instead.”

Problem 7: Multi-wire circuit diagrams with vertical connectors require position:absolute

What happened: The CNOT gate in the Phase Kickback post required a vertical line connecting a control dot (●) on one wire to an XOR circle (⊕) on another. Multiple approaches failed: border-bottom/border-top on adjacent table cells created horizontal bars instead of a vertical line; rowspan="2" in real <table> elements was visually correct but the WordPress theme’s CSS overrode <td> padding, creating unwanted row spacing that inline styles alone could not override.

Fix: A three-part solution:

  • Avoid theme td overrides: use <div style="display:table-cell"> instead of <td> — theme CSS targeting td selectors does not apply to divs.
  • Vertical connector: two separate display:flex; height:28px rows inside a position:relative; display:inline-block wrapper, with a position:absolute; width:2px connector div at a precisely calculated left value.
  • Asymmetric wire stubs: the control dot (12px wide) needs 21px wire stubs on each side; the XOR circle (20px wide) needs 17px stubs — these different widths keep both symbol centres at the same x-coordinate so the connector aligns with both. Wire stubs connect directly to the symbols with no padding gap between them.

Instruction: “For multi-wire circuits with vertical connections, use two flex rows in a position:relative wrapper with a position:absolute vertical connector. Wire stubs must touch each gate symbol directly with no padding gap.”

Tip: Let Claude Choose Categories and Tags Automatically

You do not need to specify categories and tags manually. Simply ask Claude to “use appropriate categories and tags” and it will inspect the existing taxonomy on your site, infer suitable terms from the post content, and create any new ones that are genuinely needed.

5. Step-by-Step: Posting via Claude and WordPress MCP

The following steps reproduce the exact workflow used to create and publish posts on this blog.

Step 1 — Enable the WordPress Connector in claude.ai

Go to claude.ai → Settings → Integrations. Find the WordPress.com connector and click Connect. Authorise it with your WordPress.com account.

Step 2 — Prepare your content and prompt Claude

Provide Claude with the raw content, a reference post URL, and the constraint set:

Generate a WordPress post as a custom HTML block.
Use inline CSS only — no <style> tags, no <script> tags, no SVG.
Never use &nbsp; for alignment or Unicode box-drawing characters.
Use display:flex and inline-block spans for diagrams and vectors.
For multi-wire circuits: use position:relative wrapper + position:absolute connector.
Match the styling of: [URL of a reference post]
Topic: [your content here]

Step 3 — Review the artifact in claude.ai

Review layout, colours, and diagram rendering. Verify no <style> or <script> tags appear. Check that circuit wires touch gate symbols directly.

Step 4 — Trigger the WordPress MCP posting

Post this to myhlow.wordpress.com using the WordPress connector.
Use appropriate categories and tags.
Status: draft

Claude will call categories.list and tags.list, create any missing terms, then execute posts.create and return the draft URLs.

Step 5 — Preview in WordPress and publish

Open the draft URL. If the Custom HTML block renders correctly, click Publish.

6. Installing the Workflow as a Reusable Skill

All the constraints from this guide are packaged as a downloadable Claude Skill. Once installed, the skill auto-loads every time you start a WordPress publishing task, so you never need to paste the prompt template manually again.

Step 1 — Enable Code Execution and File Creation

Go to Settings → Capabilities and toggle on Code Execution and File Creation. This is required for Claude to process the skill file when you upload it.

Step 2 — Open the Skills page

Navigate to https://claude.ai/customize/skills. Note: the Customize menu is only available in the browser version of Claude, not the mobile app. On mobile, use the direct link or enable Desktop site in your browser menu.

Step 3 — Upload the skill file

Click “+”“+ Create skill”“Upload a skill” → select wordpress-publish.skill → toggle it on.

Step 4 — Trigger in any new chat

In any new conversation, say “publish to WordPress” and Claude automatically loads all the constraints from this guide.

7. Quick Reference: Dos and Don’ts

DoDon’t
Use style="..." on every elementUse <style> blocks or CSS classes
Use display:flex + inline-block for single-wire gate diagrams and vectorsUse &nbsp; spacing or Unicode box-drawing characters for alignment
Use position:relative wrapper + position:absolute connector for multi-wire circuits; wire stubs touching symbols directlyUse border-top/border-bottom on table cells to simulate vertical connectors
Use <div style="display:table-cell"> to avoid WordPress theme td padding overridesRely on inline padding on actual <td> elements — theme CSS may override it
Wrap content in <!-- wp:html -->Use <svg> elements or <script> tags
Use lowercase "order": "desc" in API paramsUse uppercase "order": "DESC"
Post as draft first and preview in WordPressPublish directly without previewing the rendered block
Let Claude auto-select categories and tagsCreate taxonomy terms without checking for existing ones first
Set table header styles on <tr>, not <th>Apply background colours to <th> elements

Tip: Use a Reference Post to Anchor the Style

Giving Claude the URL of an existing post on your blog and asking it to match the styling is the fastest way to maintain visual consistency. The Word Embeddings post and the SingIDBot guide were both created this way, borrowing section card layout, banner colours, and code block styles from earlier posts in the series.

This article was generated with the assistance of Claude by Anthropic and posted via the WordPress MCP connector. ✨

Agentic Code Generation with OpenAI Codex CLI — A Knight’s Tour Walkthrough

Hands-On with OpenAI Codex CLI

Agentic Code Generation with OpenAI Codex CLI

A step-by-step walkthrough of using OpenAI’s agentic coding tool to scaffold, solve, test, and visualise the classic Knight’s Tour chess problem — entirely through natural language prompts.

This post assumes you have Codex CLI installed and authenticated. We’ll be working in three phases, each driven by a carefully crafted Codex prompt.

♞ What is the Knight’s Tour Problem?

The Knight’s Tour is a classic puzzle from combinatorics and graph theory: given an n×n chessboard and a knight placed on any starting square, can the knight visit every square on the board exactly once using only valid knight moves? A knight moves in an L-shape — two squares in one direction and one square perpendicular, giving it up to eight possible moves from any position.

The problem has been studied for over a thousand years. Arab mathematicians documented it as early as the 9th century, and Leonhard Euler conducted a systematic mathematical analysis in 1759. There are two variants: an open tour, where the starting and ending squares differ, and a closed (re-entrant) tour, where the knight can return to its starting square in one move. On the standard 8×8 board, there are over 26 trillion distinct open tours.

From an algorithmic standpoint, the Knight’s Tour is a special case of the Hamiltonian path problem on a graph, where each square is a node and edges connect squares reachable by a knight move. Finding a Hamiltonian path is NP-complete in general, but the regular structure of the chessboard makes efficient heuristics possible.

The most well-known heuristic is Warnsdorff’s rule (H.C. von Warnsdorff, 1823): at each step, move to the unvisited square that has the fewest onward moves. This greedy approach runs in linear time relative to the number of squares and finds a tour almost always on boards of size 5×5 and above — which is exactly what we’ll ask Codex to implement.

🗺️ What We’re Building

By the end of this walkthrough, we’ll have a fully working Python application with three layers built up incrementally through Codex prompts:

PhaseWhat Codex BuildsOutput
Phase 1Core solver using Warnsdorff’s heuristicCLI app, ASCII board output
Phase 2Pytest test suite and colourised terminal outputANSI colour board, passing tests
Phase 3Flask web app with animated HTML canvasInteractive browser visualisation

⚙️ Prerequisites

Before starting, make sure you have:

  • Codex CLI installed (brew install --cask codex or npm i -g @openai/codex)
  • Authenticated with a ChatGPT Plus/Pro account or an OpenAI API key
  • Python 3.10+ available in your shell
  • Git installed on your machine (see Project Setup below if you haven’t configured it yet)

📁 Project Setup

We’ll use Git throughout this guide as a safety net — you can git diff to review what Codex changed, or revert entirely if a phase goes wrong. If this is your first time using Git on this machine, configure your identity first:

git config --global user.name  "Your Name"
git config --global user.email "[email protected]"

This only needs to be done once. To verify your settings at any time:

git config --global --list

Now create the project directory, initialise the repo, and add an empty conftest.py in the root. The conftest.py file signals to pytest that the project root is the base directory, which allows it to resolve imports like from knight_tour import solve correctly from within the tests/ subdirectory:

mkdir knights-tour && cd knights-tour
git init
touch conftest.py
git commit --allow-empty -m "initial commit"

With the repo ready, launch Codex from inside the project directory:

codex

Once the TUI loads, your very first prompt should be to generate an AGENTS.md file. This acts as a standing project-level system prompt — Codex reads it automatically at the start of every future session, so you don’t have to repeat your conventions each time.

📝 Codex Prompt — Generate AGENTS.md

“Create an AGENTS.md file in the project root for a Python CLI and web application project. Include the following standing instructions: use Python 3.10+ with type hints throughout; place all tests in a /tests directory using pytest; never use global mutable state; prefer functions over classes unless OOP is genuinely the better fit; handle all CLI arguments with argparse; use Flask for any web routes; keep each module focused on a single responsibility. Format it as a markdown file with a brief intro line followed by a bullet list.”

Codex will create the file and show you the diff to review. Accept it, then commit before moving to Phase 1. Every subsequent Codex session in this directory will pick up these rules automatically.

💡 Why prompt Codex to write AGENTS.md instead of writing it yourself? Two reasons. First, you describe your intent conversationally rather than worrying about format. Second, it sets the right mental model for the rest of the guide — Codex writes the files, you review the diffs.

You can also update it at any time: “Add a rule that all functions must have docstrings” or “Update AGENTS.md to say we’re now using FastAPI instead of Flask” — Codex will edit the file in place and show you the diff.

Phase 1 — Core Solver

Still in the same Codex session, enter the Phase 1 prompt. Codex already has the project context from AGENTS.md, so you can get straight to the point:

📝 Codex Prompt — Phase 1

“Create a Python CLI app that solves the Knight’s Tour problem. Use Warnsdorff’s heuristic: at each step, move to the unvisited square with the fewest onward moves. The board size and starting position should be configurable via argparse using the argument names –size (default 8), –row (default 0), and –col (default 0). Do not use single-letter short flags. The solver should return None if no tour is found. Display the completed board as a grid of move numbers, right-aligned. Save the solver logic in knight_tour.py and the entry point in main.py. Include a requirements.txt (even if empty for now) and a .gitignore for Python.”

Codex will show you its plan before making any changes. In Auto mode, you’ll see it create each file with a diff preview. Here’s a simplified version of what the generated knight_tour.py looks like:

MOVES = [(2,1),(2,-1),(-2,1),(-2,-1),(1,2),(1,-2),(-1,2),(-1,-2)]

def get_neighbours(x, y, n, visited):
    return [(x+dx, y+dy) for dx,dy in MOVES
            if 0 <= x+dx < n and 0 <= y+dy < n
            and not visited[x+dx][y+dy]]

def solve(n, start_x, start_y):
    visited = [[False]*n for _ in range(n)]
    board   = [[-1]*n   for _ in range(n)]
    x, y = start_x, start_y
    visited[x][y] = True
    board[x][y]   = 0
    for move in range(1, n*n):
        neighbours = get_neighbours(x, y, n, visited)
        if not neighbours:
            return None
        x, y = min(neighbours,
                   key=lambda p: len(get_neighbours(p[0],p[1],n,visited)))
        visited[x][y] = True
        board[x][y]   = move
    return board

Run it in a separate terminal to verify:

python3 main.py --size 8 --row 0 --col 0

You should see a numbered 8×8 grid where each number represents the move order of the knight. Commit the result before moving to Phase 2.

💡 Approval Flow: In Codex’s default Auto mode, you’ll see a diff for each file before it’s written. Press A to accept or R to reject. If Codex proposes something you don’t want, reject it and follow up with a corrective prompt — it retains full context.

Phase 2 — Tests & Colourised Output

In the same Codex session, enter the next prompt. You don’t need to re-explain the project — Codex still has full context from Phase 1.

Before entering the prompt, install pytest in a separate terminal. Codex will attempt to run the test suite as part of its workflow, so the package must be present beforehand:

pip3 install pytest
📝 Codex Prompt — Phase 2

“Now add two things. First, create a test suite in tests/test_knight_tour.py using pytest. Test that: (1) solve() returns a valid tour where every integer from 0 to n²−1 appears exactly once, (2) consecutive moves are a valid knight’s move apart, (3) solve() returns None for n=2 which has no solution. Second, add a new function print_coloured_board() in knight_tour.py that uses ANSI escape codes to colour the board — alternate between a light and dark background for a chess-style pattern, with white text for the move numbers. Call it from main.py instead of format_board() when –colour flag is passed.”

Codex will generate the test file and extend knight_tour.py with the colour function. Key tests:

import pytest
from knight_tour import solve

def is_valid_knight_move(x1, y1, x2, y2):
    dx, dy = abs(x2-x1), abs(y2-y1)
    return (dx, dy) in {(1,2),(2,1)}

@pytest.mark.parametrize("n,r,c", [(5,0,0),(6,1,1),(8,0,0),(8,3,4)])
def test_valid_tour(n, r, c):
    board = solve(n, r, c)
    assert board is not None
    flat = sorted(v for row in board for v in row)
    assert flat == list(range(n*n))

def test_no_solution_n2():
    assert solve(2, 0, 0) is None

Run the tests in a separate terminal to verify. If you created conftest.py in the project root during setup, pytest will resolve imports correctly:

pytest tests/ -v

If you see a ModuleNotFoundError: No module named 'knight_tour', use the module invocation instead, which explicitly adds the project root to sys.path:

python3 -m pytest tests/ -v

And try the colour flag:

python3 main.py --size 8 --colour

Phase 3 — Flask Web App with Animated Visualisation

Now we go beyond the terminal. In the same session (or a fresh one — Codex resumes from codex resume --last), enter the Phase 3 prompt:

📝 Codex Prompt — Phase 3

“Add a Flask web app in app.py. It needs two routes: GET / serves a single-page HTML form where users can input board size (5–10) and starting row/col. POST /solve accepts these inputs, runs the solver, and returns the board as JSON. The HTML page should also contain a JavaScript canvas visualisation that animates the knight’s path one move at a time when the solution arrives — draw the board as a grid, colour visited squares progressively, and draw a ♞ symbol on the current square. Add flask to requirements.txt.”

Codex generates app.py and embeds the full HTML/JS using Flask’s render_template_string. The key backend endpoint:

from flask import Flask, request, jsonify, render_template_string
from knight_tour import solve
app = Flask(__name__)

@app.route("/solve", methods=["POST"])
def solve_tour():
    data = request.get_json()
    n, row, col = int(data.get("size",8)), int(data.get("row",0)), int(data.get("col",0))
    if not (5 <= n <= 10):
        return jsonify({"error": "Board size must be between 5 and 10"}), 400
    board = solve(n, row, col)
    if board is None:
        return jsonify({"error": "No solution found"}), 422
    return jsonify({"board": board, "n": n})

Install Flask and run:

pip3 install flask
python3 app.py

Visit http://localhost:5000, choose your board size and starting square, hit Solve, and watch the knight’s path animate across the canvas.

🔄 Iterating Further — More Prompt Ideas

Once your three-phase app is working, you can keep iterating in the same session. Here are some prompts to take it further:

GoalFollow-up Prompt
Speed comparison“Add a backtracking solver as an alternative to Warnsdorff’s. Add a –solver flag to switch between them, and time both with Python’s timeit.”
Export result“Add a –export flag to main.py that saves the board as a CSV and also renders it as a PNG using matplotlib, with the knight path drawn as a line.”
User clicks board“Update the web app so users click a cell on the canvas to set the starting position instead of using the form fields.”
Code review“Review the current codebase for edge cases, type annotation completeness, and any issues with the input validation in app.py.”

✅ Effective Prompting Tips for Codex

A few patterns that made a noticeable difference in the quality of output across this walkthrough:

TipWhy It Helps
Name your files explicitlyCodex won’t guess at naming conventions. Saying “save to knight_tour.py” prevents it from choosing arbitrary filenames.
Specify what None/failure meansWithout “return None if no tour is found,” Codex might raise an exception instead — a valid choice but harder to test.
Bundle related changes in one promptPhase 2 added tests and colour output together. Codex handles multi-file tasks well when given in a single cohesive prompt.
Keep sessions alive for follow-upsDon’t exit and re-enter. Staying in the same session means Codex retains full context — you can make corrections without re-explaining the project.
Use AGENTS.md for standing rulesAnything you’d say in every prompt belongs in AGENTS.md. It keeps prompts shorter and ensures consistent style across sessions.

📁 Final Project Structure

knights-tour/
├── AGENTS.md
├── conftest.py           ← empty, anchors pytest to project root
├── .gitignore
├── requirements.txt      ← flask
├── knight_tour.py        ← solver + display logic
├── main.py               ← CLI entry point
├── app.py                ← Flask web app
└── tests/
    └── test_knight_tour.py

What’s worth noticing in this walkthrough is the workflow rhythm: each Codex prompt builds on the last without re-explaining context, and the AGENTS.md file silently enforces project conventions so you don’t have to. The three phases took roughly 15 minutes end-to-end, most of which was reviewing diffs and running tests rather than writing code.

The Knight’s Tour is just the illustration. The same prompt-iterate-commit loop applies to any project — and the more you invest in a good AGENTS.md upfront, the more useful each Codex session becomes.

Word Embeddings Explained: The Math Behind AI, LLMs, and Chatbots

NLP Explainer · AI Series 2026

How Machines Understand Language

A guide to word embeddings — where meaning becomes mathematics, and vectors do the talking.

When a search engine retrieves a document about automobiles in response to a query about cars, it is not matching text character by character. Somewhere beneath the interface, the system understands that these two words are semantically related. The mechanism behind that understanding is the word embedding — and once you see the geometry, you cannot unsee it.

This article walks through the key mathematical operations that make embeddings work: distance, similarity, arithmetic, scaling, and the dot product. Each concept is illustrated with concrete numerical vectors so the math is visible, not just described. Real embeddings typically use hundreds of dimensions; the 3- and 4-dimensional examples here preserve all the structure while staying readable on a page.

1  ·  What is a Word Embedding?

A word embedding is a representation of a word as a vector — an ordered list of numbers — in a high-dimensional space. A typical embedding model might use 300 dimensions, so the word cat becomes a point with 300 coordinates. That sounds abstract, but the key insight is this: the position of that point encodes meaning.

This is what researchers call a semantic space. Words with related meanings end up positioned close to each other. King and Queen live near each other. Paris and London live near each other. Bicycle and democracy live far apart. The model learns these positions not from human-curated rules, but from the statistical patterns of how words appear together in enormous text corpora.

EXAMPLE: 4-DIMENSIONAL VECTORS (simplified from real 300-dim embeddings)
vec(“King”)  = [ 0.9, 0.7, 0.4,  +0.6 ]
vec(“Queen”)  = [ 0.9, 0.7, 0.4,  -0.6 ]
vec(“Man”)  = [ 0.5, 0.3, 0.1,  +0.8 ]
vec(“Woman”) = [ 0.5, 0.3, 0.1,  -0.8 ]

The first three dimensions encode royalty, authority, and age.
The fourth dimension encodes gender: positive = masculine, negative = feminine.

Think of it as a map where the geography is meaning. Every word is a pin, and the distances between pins reflect semantic relationships rather than physical ones.

2  ·  The Geometry of Meaning: Distance and Similarity

Once words are points in space, we need a way to measure how close they are. Two approaches dominate: Euclidean distance and cosine similarity. For the examples below, we use a 3-dimensional temperature embedding:

TEMPERATURE VECTORS (3 dimensions)
vec(“Hot”) = [  1.0,  0.8,  0.6 ]
vec(“Warm”) = [  0.8,  0.6,  0.4 ]
vec(“Cold”) = [ -0.6,  0.4, -0.8 ]

2.1   Euclidean (Cartesian) Distance

The most intuitive measure — the straight-line gap between the tips of two arrows drawn from the origin. For vectors a and b in n dimensions:

d(a, b)  =  √ Σi ( aibi )2
WORKED EXAMPLE: EUCLIDEAN DISTANCE
// Hot vs Warm (similar words)
d(Hot, Warm) = √[ (1.0-0.8)2 + (0.8-0.6)2 + (0.6-0.4)2 ]
              = √[ 0.04 + 0.04 + 0.04 ] = √0.12  &approx;  0.346  ← small: close together

// Hot vs Cold (opposite words)
d(Hot, Cold) = √[ (1.0-(-0.6))2 + (0.8-0.4)2 + (0.6-(-0.8))2 ]
              = √[ 2.56 + 0.16 + 1.96 ] = √4.68  &approx;  2.163  ← large: far apart

2.2   Cosine Similarity — The Industry Standard

In practice, NLP systems almost universally prefer cosine similarity over Euclidean distance. It ignores the length of vectors entirely and focuses only on the angle between them — two vectors pointing the same direction score 1.0 regardless of their magnitude.

COSINE SIMILARITY
cos(θ)  = a  ·  b
a‖  ×  ‖b
Range: −1  (opposite)  →  0  (orthogonal)  →  +1  (identical direction)
WORKED EXAMPLE: COSINE SIMILARITY
// First compute magnitudes
‖Hot‖ = √(1.02 + 0.82 + 0.62) = √2.00 &approx; 1.414
‖Warm‖ = √(0.82 + 0.62 + 0.42) = √1.16 &approx; 1.077
‖Cold‖ = √(0.62 + 0.42 + 0.82) = √1.16 &approx; 1.077

// Hot vs Warm (small angle)
dot(Hot, Warm) = (1.0)(0.8) + (0.8)(0.6) + (0.6)(0.4) = 0.80 + 0.48 + 0.24 = 1.52
cos(Hot, Warm) = 1.52 / (1.414 × 1.077) = 1.52 / 1.523 &approx; +0.998

// Hot vs Cold (large angle)
dot(Hot, Cold) = (1.0)(-0.6) + (0.8)(0.4) + (0.6)(-0.8) = -0.60 + 0.32 – 0.48 = -0.76
cos(Hot, Cold) = -0.76 / (1.414 × 1.077) = -0.76 / 1.523 &approx; -0.499
Word Pair Euclidean d cos(θ) Interpretation
Hot vs Warm 0.346 +0.998 Nearly identical direction — closely related
Hot vs Cold 2.163 −0.499 Opposite directions — antonyms
3  ·  Vector Arithmetic: Meaning You Can Add and Subtract

Because words are vectors, you can perform arithmetic on them — and the results are semantically meaningful. The most famous example uses the 4-dimensional royalty vectors introduced in Section 1:

THE CLASSIC ANALOGY
vec(“King”) − vec(“Man”) + vec(“Woman”)  &approx;  vec(“Queen”)
WORKED EXAMPLE: KING – MAN + WOMAN
King  = [ 0.9,  0.7,  0.4,  +0.6 ]
Man  = [ 0.5,  0.3,  0.1,  +0.8 ]
Woman  = [ 0.5,  0.3,  0.1,  -0.8 ]

// Subtract component by component, then add
King – Man = [ 0.9-0.5,  0.7-0.3,  0.4-0.1,  0.6-0.8 ] = [  0.4,   0.4,   0.3,  -0.2 ]
+ Woman   = [ 0.4+0.5,  0.4+0.3,  0.3+0.1,  -0.2+(-0.8) ] = [  0.9,   0.7,   0.4,  -1.0 ]

// Find nearest word by Euclidean distance
result = [ 0.9, 0.7, 0.4, -1.0 ]

d(result, Queen) = √[ 0 + 0 + 0 + (-1.0-(-0.6))2 ] = √0.16 &approx; 0.400 ← nearest
d(result, Woman) &approx; 0.671    d(result, King) = 1.600    d(result, Man) &approx; 1.910

cos(result, Queen) &approx; 0.974   ← highest cosine similarity also points to Queen

What happened geometrically? Subtracting Man from King stripped out the gender dimension (+0.8 gone), leaving the royalty structure intact. Adding Woman injected the feminine gender value (-0.8). The result sits 0.4 units from Queen — the nearest word in this vocabulary.

4  ·  Scalar Multiplication and Division: Changing Intensity

Multiplying or dividing a vector by a scalar (a plain number) changes its magnitude without changing its direction. This maps neatly onto the idea of degree in language — Tiny, Large, and Gigantic all point in roughly the same semantic direction, but at different intensities.

SIZE VECTORS (3 dimensions)
vec(“Tiny”) = [ 0.10, 0.20, 0.10 ]
vec(“Large”) = [ 0.50, 0.70, 0.40 ]
vec(“Gigantic”) = [ 1.10, 1.50, 0.90 ]
WORKED EXAMPLE: SCALING ALONG THE SIZE AXIS
// Multiplying Large by 2 moves it toward Gigantic
Large × 2 = [ 0.5×2,  0.7×2,  0.4×2 ] = [ 1.00,  1.40,  0.80 ]
vec(“Gigantic”) = [ 1.10,  1.50,  0.90 ]    d(Large × 2, Gigantic) &approx; 0.173 ← very close

// Multiplying Large by 0.2 moves it toward Tiny
Large × 0.2 = [ 0.10,  0.14,  0.08 ]
vec(“Tiny”) = [ 0.10,  0.20,  0.10 ]    d(Large × 0.2, Tiny) &approx; 0.063 ← very close

Division works the same way along an intensity axis. Halving a “Loud” vector lands near “Soft”:

WORKED EXAMPLE: DIVIDING ALONG THE LOUDNESS AXIS
vec(“Loud”) = [ 0.90, 1.20, 0.60 ]    vec(“Soft”) = [ 0.30, 0.40, 0.20 ]
Loud ÷ 2 = [ 0.45,  0.60,  0.30 ]
d(Loud ÷ 2, Soft) &approx; 0.269  ← direction unchanged, intensity halved
Key intuition: Scalar operations change how much of something a vector represents, without changing what kind of thing it represents. Direction is preserved; intensity is tuned.
5  ·  The Dot Product: Agreement and Magnitude Together

The dot product of two vectors is computed by multiplying their corresponding components and summing the results:

DOT PRODUCT
a  ·  b  =  Σi  ( ai × bi )  =  a1b1  +  a2b2  + … +  anbn

The dot product is cosine similarity before normalising away the vector lengths. It captures two things simultaneously: the direction of agreement and the combined magnitude. Cosine similarity captures only the first.

We reuse the loudness vectors from Section 4 — Very Loud is “Loud” and A Little Loud is “Soft”. They point in exactly the same direction but have very different lengths:

WORKED EXAMPLE: VERY LOUD vs A LITTLE LOUD
vec(“A Little Loud”) = [ 0.30, 0.40, 0.20 ]  |magnitude| = 0.539
vec(“Very Loud”) = [ 0.90, 1.20, 0.60 ]  |magnitude| = 1.616

// Cosine similarity: measures direction only
dot(AL, VL) = (0.3)(0.9) + (0.4)(1.2) + (0.2)(0.6) = 0.27 + 0.48 + 0.12 = 0.87
cos(AL, VL) = 0.87 / (0.539 × 1.616) = 0.87 / 0.871 &approx; 1.000

// Dot product: measures direction AND magnitude
AL · AL = (0.3)2 + (0.4)2 + (0.2)2 = 0.09 + 0.16 + 0.04 = 0.29
VL · VL = (0.9)2 + (1.2)2 + (0.6)2 = 0.81 + 1.44 + 0.36 = 2.61
Comparison Magnitude cos(θ) v · v
A Little Loud 0.539 1.000 (same dir.) 0.29
Very Loud 1.616 1.000 (same dir.) 2.61

Both words are perfectly collinear — cosine similarity is 1.0 in both cases. But the dot products are 0.29 vs 2.61, a 9× difference. This is why recommendation systems and attention mechanisms in transformer models often prefer raw dot products: when you want to know not just whether a document is relevant but also how prominently it discusses a topic, the dot product gives you both dimensions at once.

6  ·  Practical Applications

Search engines convert your query into a vector and retrieve documents whose vectors are nearest to it in the semantic space — using cosine similarity to rank by relevance regardless of exact word match. When you search for car insurance and the engine returns results about vehicle coverage, it is doing nearest-neighbour lookup in embedding space, exactly as the Hot/Warm/Cold example in Section 2 demonstrates.

Recommendation systems represent your interests as a vector computed from your history, then find products whose vectors are closest to yours. The dot product is particularly useful here: a highly-relevant item with a large magnitude — analogous to Very Loud — will score higher than a mildly-relevant item even if they point in the same direction.

Large language models use the scaled dot product directly inside the attention mechanism. For every token, a query vector and a set of key vectors are compared via dot product to determine which parts of the context deserve attention — a direct descendant of the arithmetic explored in Section 5.

Quick Reference: Embedding Operations

Operation Formula Section 2-5 Result
Euclidean Distance √( Σ (aibi)2 ) d(Hot,Warm) = 0.346   d(Hot,Cold) = 2.163
Cosine Similarity (a·b) / (‖a‖×‖b‖) cos(Hot,Warm) = +0.998   cos(Hot,Cold) = -0.499
Vector Arithmetic a ± b King-Man+Woman → nearest Queen (d = 0.400)
Scalar Multiplication λ · a Large × 2 → near Gigantic   Loud ÷ 2 → near Soft
Dot Product a·b = Σ aibi cos = 1.00 for both; dot 0.29 (soft) vs 2.61 (loud)

✦ This article was generated with the assistance of Claude by Anthropic

SingIDBot — Telegram ID Retrieval Bot for OpenClaw Topic Configuration

SETUP GUIDE
Build and deploy SingIDBot — a Telegram bot that retrieves Chat IDs and Topic Thread IDs, with a Singlish personality — on PythonAnywhere.

SingIDBot — Telegram ID Retrieval Bot for OpenClaw Topic Configuration

If you have been working through the OpenClaw Telegram integration guide, you already know that one of the trickiest steps is obtaining the correct Chat ID and Topic Thread ID before you can configure your bot endpoints. Getting those IDs wrong means your bot silently sends messages into the void — no errors, no feedback, just nothing arriving in the right place.

SingIDBot was built specifically to solve that problem. Drop it into any Telegram chat or group, fire the /getids command, and it will instantly report back the Chat ID and — crucially — the message_thread_id for whichever topic the command was sent from. No fiddling with Telegram’s Bot API directly, no JSON parsing, no guesswork.

Beyond the ID retrieval utility, SingIDBot has a second personality: it responds to free-text messages in Singlish — Singapore’s beloved colloquial English creole — cycling through randomised phrases like “Aiyoh, you finally came! Welcome lah!” and “Wah, interesting leh. Tell me more can?” This makes the bot noticeably more fun to test in group chats, and also serves as a practical demonstration of keyword-based message classification in python-telegram-bot v20+.

This guide walks through the complete source code, explains each architectural decision, and then covers deployment on PythonAnywhere including the free-tier network restrictions you will inevitably hit and exactly how to handle them.

Prerequisites
Requirement Details
Python 3.10+ Required for python-telegram-bot v20+ async support. PythonAnywhere provides Python 3.10 on all plans.
python-telegram-bot v20+ The async-native version of the library. Install with pip install python-telegram-bot. Version 20 introduced the Application.builder() pattern used throughout this bot.
Telegram Bot Token Create a new bot via @BotFather on Telegram. Send /newbot, follow the prompts, and copy the token. Keep it secret.
PythonAnywhere account A free Beginner account is sufficient to run and test the bot, but has an important network restriction covered in Section 3. A paid plan removes that restriction entirely.
Section 1 — What SingIDBot Does

The message_thread_id Mechanism

Telegram supergroups can be divided into named Topics, each identified by a message_thread_id. When a message is sent inside a topic, Telegram attaches this integer to the message object. When sent outside any topic, the field is absent entirely — not zero, not null, but genuinely missing.

This is the exact value OpenClaw requires to route notifications to the correct topic thread. SingIDBot’s /getids command reads message_thread_id from the incoming message and reports it in plain text for direct copy-paste into your configuration.

Because the field may be absent, the code uses getattr(update.effective_message, ‘message_thread_id’, None) rather than direct attribute access, which would raise an AttributeError in non-topic contexts.

The Singlish Responder

Any free-text message (not a command) triggers the singlish_chat handler. It classifies the message into greeting, thanks, or default using keyword matching, then picks a random phrase from the corresponding pool. A guard clause returns early if the message starts with / or was sent by the bot itself, preventing an infinite echo loop.

Section 2 — Code Walkthrough

2.1 Logging Setup

Two handlers run simultaneously: a FileHandler writing UTF-8 lines to singidbot.log, and a StreamHandler for the console. On PythonAnywhere you can watch live output in the bash session and audit the full log file afterward.

LOG_FILE = "singidbot.log"
logging.basicConfig(
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    level=logging.INFO,
    handlers=[
        logging.FileHandler(LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(),
    ],
)
logger = logging.getLogger(__name__)
Tip: The encoding=”utf-8″ argument is essential if any Telegram usernames contain non-ASCII characters. Without it, a single emoji in a username can crash the logger with a UnicodeEncodeError.

2.2 Singlish Response Bank

SINGLISH_RESPONSES maps category keys to phrase lists. get_singlish_response() uses dict.get() with a “default” fallback so unknown categories never raise a KeyError, and random.choice() picks uniformly from the pool.

SINGLISH_RESPONSES = {
    "greeting": [
        "Wah, you here ah! Come come, sit down lah!",
        "Eh hello! Long time no see, how are you?",
        "Aiyoh, you finally came! Welcome lah!",
        "Oi! Good to see you sia. What can I help you?",
    ],
    "thanks": [
        "Aiyah, no need to thank one lah!",
        "Wah, so polite! Welcome lah, anytime!",
        "No problem one! Next time also can help you.",
        "Eh, paiseh lah - that is what I am here for!",
    ],
    "default": [
        "Hah? Can repeat or not? I blur blur lah.",
        "Wah, interesting leh. Tell me more can?",
        "Aiyoh, I also dunno how to answer you sia.",
        "Got it lah! But I not so sure about that one.",
        "Eh, you very clever leh. I learn from you!",
    ],
}

def get_singlish_response(category):
    pool = SINGLISH_RESPONSES.get(category, SINGLISH_RESPONSES["default"])
    return random.choice(pool)

2.3 log_activity() Async Helper

log_activity() is declared async to match the handler context. It resolves the user’s display identifier to @username or numeric id, and emits a structured log line with a UTC ISO-8601 timestamp.

async def log_activity(user, event_type, metadata):
    user_info = (
        f"@{user.username}" if user and user.username
        else f"id={user.id}" if user
        else "unknown"
    )
    timestamp = datetime.now(timezone.utc).isoformat()
    logger.info(
        "[%s] event=%s user=%s metadata=%s",
        timestamp, event_type, user_info, metadata
    )

2.4 /start and /help Handlers

Both handlers log the event first, then compose and send a reply. /start personalises the greeting using user.first_name with a fallback to “friend”, prepending a random Singlish greeting to make every first interaction slightly different.

async def start(update, context):
    user = update.effective_user
    await log_activity(user, "START", {"chat_id": update.effective_chat.id})
    greeting = get_singlish_response("greeting")
    name = user.first_name if user and user.first_name else "friend"
    welcome_text = (
        f"{greeting}\n\nEh {name}, I am *SingIDBot* lah!\n\nCommands:\n- /start\n- /help\n- /getids"
    )
    await update.message.reply_text(welcome_text, parse_mode="Markdown")

async def help_command(update, context):
    user = update.effective_user
    await log_activity(user, "HELP", {"chat_id": update.effective_chat.id})
    await update.message.reply_text("*SingIDBot Commands* lah!\n/start /getids /help", parse_mode="Markdown")

2.5 /getids Handler

The key line is the getattr call. Telegram only populates message_thread_id when a message arrives inside a topic thread — in a regular chat the attribute simply does not exist. getattr with a None default handles this safely without raising AttributeError.

async def get_ids(update, context):
    user = update.effective_user
    await log_activity(user, "ID_RETRIEVAL", {"chat_id": update.effective_chat.id})
    chat_id = update.effective_chat.id
    topic_id = getattr(update.effective_message, "message_thread_id", None)
    response = f"Chat/Group ID: {chat_id}\n"
    if topic_id is not None:
        response += f"Topic ID: {topic_id}\n(This ID confirms the specific topic thread)"
    else:
        response += "Topic ID: Not available\n(Topics may not be enabled)"
    response += "\nTip: Record these IDs for configuring bot endpoints!"
    await update.message.reply_text(response, parse_mode="Markdown")
    await update.message.reply_text(f"{get_singlish_response('thanks')} Lah!")

2.6 singlish_chat Handler

Registered with filters.TEXT and ~filters.COMMAND. An internal guard also checks if the sender is the bot itself — without this, the bot’s own replies would trigger further replies in an infinite loop. Keyword classification checks thanks before greeting to avoid misclassification on mixed messages.

async def singlish_chat(update, context):
    user = update.effective_user
    await log_activity(user, "MESSAGE", {"text_preview": update.message.text[:50] if update.message else "N/A"})
    if (not update.message or update.message.text.startswith("/") or update.effective_user.id == context.bot.id):
        return
    user_text = update.message.text.lower().strip()
    if any(w in user_text for w in ["thanks", "thank", "appreciate"]):
        category = "thanks"
    elif any(w in user_text for w in ["hello", "hi", "hey", "oi", "wah", "alamak"]):
        category = "greeting"
    else:
        category = "default"
    await update.message.reply_text(get_singlish_response(category))

2.7 error_handler — Two-Tier Approach

Transient network issues (ProxyError, NetworkError, Conflict) are logged at WARNING level as a single line with no stack trace. Genuine bugs fall through to ERROR level with full exc_info traceback so they remain visible and actionable.

async def error_handler(update, context):
    error = context.error
    if isinstance(error, Exception):
        error_name = type(error).__name__
        error_msg = str(error)
        if "ProxyError" in error_msg or "NetworkError" in error_name:
            logger.warning("Network error (transient): %s", error_msg)
        elif "Conflict" in error_name or "terminated by other getUpdates" in error_msg:
            logger.warning("Conflict error (duplicate instance?): %s", error_msg)
        else:
            logger.error("Unhandled exception: %s: %s", error_name, error_msg, exc_info=context.error)

2.8 main() — Builder Chain, Timeouts, Handler Registration

All three timeout axes — connect, read, and write — are set to 30 seconds. Handlers are registered in priority order: specific commands first, then the catch-all text handler, then the error handler last.

def main():
    TOKEN = "YOUR_BOT_TOKEN_HERE"
    application = (
        Application.builder()
        .token(TOKEN)
        .connect_timeout(30)
        .read_timeout(30)
        .write_timeout(30)
        .build()
    )
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("help", help_command))
    application.add_handler(CommandHandler("getids", get_ids))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, singlish_chat))
    application.add_error_handler(error_handler)
    logger.info("--- SingIDBot Initializing ---")
    application.run_polling(allowed_updates=Update.ALL_TYPES)
Section 3 — Deployment on PythonAnywhere

Upload singidbot.py via the Files tab, open a Bash console, and run:

pip install python-telegram-bot --upgrade
python singidbot.py

Free-Tier Proxy Restriction

Free accounts route outbound traffic through a proxy that does not whitelist api.telegram.org. Outbound sendMessage calls can fail with httpx.ProxyError: 503 Service Unavailable. Inbound polling via getUpdates works fine — it is only the reply direction that is intermittently blocked.

In practice across four live sessions totalling over 5 hours, only 2 ProxyErrors occurred in 1800+ polling cycles. The error handler suppresses these to a single WARNING line so the log stays clean.

Options if you need reliable reply delivery: Upgrade to a paid PythonAnywhere plan (removes proxy restrictions entirely), or switch from polling to webhook mode via PythonAnywhere’s WSGI layer (more complex but works on the free tier).

Avoiding Duplicate Instance Conflicts

Telegram only allows one active getUpdates poller per token. Always kill the old process before restarting:

pkill -f singidbot.py
pgrep -a -f singidbot
python singidbot.py
Section 4 — Log Analysis

The bot was tested across four live sessions on PythonAnywhere’s free tier.

Session Duration Key Events Errors
Session 1 ~2 min First run. Hello received; /getids succeeded returning Chat ID 5024469893. 1 ProxyError on reply (full traceback, no error handler yet).
Session 2 ~3 min Error handler added. /help ProxyError suppressed to single WARNING. /getids worked. Singlish replies all succeeded. 1 ProxyError (WARNING only).
Session 3 ~14 min Extended run. Second /help succeeded. 47 consecutive clean polling cycles. 1 ProxyError (WARNING only).
Session 4 ~5 hours 1800+ log lines. Clean polling throughout. No conflicts, no crashes. 2 ProxyErrors total across the entire session.

SingIDBot is production-stable on the free tier. The intermittent ProxyError is a platform constraint, not a code defect, and the error handler keeps it from polluting the log.

Quick Reference
Command What it returns Notes
/start Personalised Singlish greeting and command list. Works in any chat context.
/help List of all available commands. Works in any chat context.
/getids Chat ID and Topic Thread ID (if inside a topic). Must be run inside the target topic to get a valid Thread ID.

✦ This article was generated with the assistance of Claude by Anthropic

Connecting an OpenClaw Bot to a Telegram Group Topic

SETUP GUIDE
Route OpenClaw agent notifications and responses through a private Telegram group using Topics for clean per-bot threading.

Prerequisites
Item Details
OpenClaw with Telegram bot OpenClaw must already be set up and working with a Telegram bot on your phone before proceeding. This guide only covers configuring it to post into a specific group topic.
Telegram account Active account with admin rights on a Telegram group
Telegram group with Topics A Telegram group with the Topics feature enabled
SingIDBot @SingIDBot — a custom bot built by the author of this guide, used to retrieve the chat ID and topic thread ID. Responds in Singlish, so don't be surprised by the personality.
1  ·  Create a Topic for the Bot

With Topics enabled on your group, create a dedicated topic for your OpenClaw bot to keep its messages isolated from other group activity.

Tap the pencil / new topic icon in your group and give it a clear name such as:

OpenClaw-Bot

Note the topic thread ID — you will retrieve this precisely in Step 2 using SingIDBot.

Tip: If the Topics toggle is not visible in your group settings, try adding 4–5 members to the group first. Telegram requires a minimum number of members before the Topics feature becomes available.
2  ·  Get IDs from SingIDBot

SingIDBot (@SingIDBot) returns the exact numeric IDs that OpenClaw needs: the group's chat ID and the topic's thread ID.

  1. Add @SingIDBot to your group and grant it member access.
  2. Navigate into your OpenClaw-Bot topic (not the General topic) before sending the command.
  3. Inside the topic, send: /getids
  4. SingIDBot will reply with a block like this:
Chat ID     : -1001234567890
Thread ID : 42
User ID     : 987654321

Copy the Chat ID and Thread ID. You will pass them to OpenClaw in Step 4.

3  ·  Ask OpenClaw to Build the Config Tool

Rather than editing the .env file manually, ask the OpenClaw agent to write a Python tool that does it for you. The tool only needs the chat ID and topic thread ID — the bot token is already present in the .env from initial setup and does not need to be passed in. Use this prompt in your OpenClaw chat interface:

Create a Python tool called update_telegram_config that accepts chat_id and topic_id as arguments and writes the values of TELEGRAM_CHAT_ID and TELEGRAM_TOPIC_ID into the OpenClaw .env file.

OpenClaw will scaffold the tool, register it, and confirm it is ready to call. Proceed to Step 4 once the agent confirms the tool is available.

4  ·  Run the Tool and Reload OpenClaw

With the IDs from Step 2 in hand, instruct OpenClaw to run the tool using your actual values:

Run update_telegram_config with: chat_id = “-1001234567890” topic_id = “42”

Replace the placeholder values with the IDs from Step 2. After the tool executes, reload OpenClaw so the new environment variables are picked up:

# In your terminal (if running OpenClaw locally) Ctrl+C # stop the current process openclaw # restart
5  ·  Test the Setup

Navigate into the OpenClaw-Bot topic in your Telegram group and post any message. Wait for your OpenClaw bot to reply.

If the group ID and topic ID have been configured correctly, your OpenClaw bot will respond directly inside the topic. If there is no response, double-check the IDs from Step 2 and rerun Step 4.

Note: By default, OpenClaw bots are configured to not respond to any message in a Telegram topic unless the correct group ID and topic ID are added into the config file. A missing or incorrect ID will result in complete silence from the bot.

SingIDBot Quick Reference

@SingIDBot is a custom Telegram bot built by the author of this guide. It does one job well: return the IDs you need for bot configuration. It also speaks Singlish, so expect replies like “Wah, your chat ID here lah” — perfectly normal, just read past the flavour text and grab your numbers.

Command What it returns Notes
/getids Chat ID, Thread ID (topic), and your User ID Must be run inside the target topic to get the correct Thread ID

Also tested on Hermes Agent: The same setup process works with the NousResearch Hermes Agent framework. The update_telegram_config tool and environment variable structure are identical — simply run the same steps with Hermes Agent in place of OpenClaw.
Your agent is now wired to a dedicated Telegram topic. Every notification, response, or alert it sends will land in the OpenClaw-Bot thread, keeping it separate from general group activity. To add more agents or tools later, repeat Steps 1–4 with a new topic and a new set of environment variable names.

✦ This article was generated with the assistance of Claude by Anthropic